mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-07 10:22:12 +00:00
Compare commits
47 Commits
replace-pr
...
fix/extern
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
305633d1ff | ||
|
|
ef80e3c589 | ||
|
|
873da01c07 | ||
|
|
da3ea40bd0 | ||
|
|
da5860297f | ||
|
|
54fca5ba44 | ||
|
|
66f4c3d6ec | ||
|
|
a0f407a848 | ||
|
|
3562de8fbb | ||
|
|
ef80fb39fd | ||
|
|
594d4dc737 | ||
|
|
01415b58be | ||
|
|
f7728c9019 | ||
|
|
3fffe6e198 | ||
|
|
4594f4ffe3 | ||
|
|
b660dc8573 | ||
|
|
c7b9c0d36c | ||
|
|
4433d22c09 | ||
|
|
9443c42334 | ||
|
|
9387c96023 | ||
|
|
ff2555d8dd | ||
|
|
85953c9a4c | ||
|
|
0155cc38cb | ||
|
|
c217cc96c3 | ||
|
|
580cf32eb5 | ||
|
|
6d3580cbfa | ||
|
|
6c5d36caa9 | ||
|
|
c4a6c7e277 | ||
|
|
c9cd974dca | ||
|
|
5b3f121431 | ||
|
|
c79373314a | ||
|
|
858cd287fa | ||
|
|
afdb674068 | ||
|
|
30a6721472 | ||
|
|
518dfcbe59 | ||
|
|
424127c27c | ||
|
|
2dcb817de1 | ||
|
|
f6f8c78aaf | ||
|
|
3c99dfdfa5 | ||
|
|
6ed72519b8 | ||
|
|
fe910aaa0f | ||
|
|
0d362b3ba8 | ||
|
|
51e9ffb847 | ||
|
|
f497a154a2 | ||
|
|
659fa361ef | ||
|
|
84e77182f6 | ||
|
|
32619869e7 |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -132,3 +132,6 @@
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
## UplotV2
|
||||
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
|
||||
7
.github/workflows/integrationci.yaml
vendored
7
.github/workflows/integrationci.yaml
vendored
@@ -42,10 +42,11 @@ jobs:
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
- dashboard
|
||||
- querier
|
||||
- ttl
|
||||
- preference
|
||||
- logspipelines
|
||||
- preference
|
||||
- querier
|
||||
- role
|
||||
- ttl
|
||||
- alerts
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -11,5 +11,8 @@
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
|
||||
}
|
||||
}
|
||||
|
||||
11
README.md
11
README.md
@@ -66,6 +66,17 @@ Read [more](https://signoz.io/metrics-and-dashboards/).
|
||||
|
||||

|
||||
|
||||
### LLM Observability
|
||||
|
||||
Monitor and debug your LLM applications with comprehensive observability. Track LLM calls, analyze token usage, monitor performance, and gain insights into your AI application's behavior in production.
|
||||
|
||||
SigNoz LLM observability helps you understand how your language models are performing, identify issues with prompts and responses, track token usage and costs, and optimize your AI applications for better performance and reliability.
|
||||
|
||||
[Get started with LLM Observability →](https://signoz.io/docs/llm-observability/)
|
||||
|
||||

|
||||
|
||||
|
||||
### Alerts
|
||||
|
||||
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.
|
||||
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.110.0
|
||||
image: signoz/signoz:v0.110.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.110.0
|
||||
image: signoz/signoz:v0.110.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.110.0}
|
||||
image: signoz/signoz:${VERSION:-v0.110.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.110.0}
|
||||
image: signoz/signoz:${VERSION:-v0.110.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -2285,6 +2285,15 @@ paths:
|
||||
deprecated: false
|
||||
description: This endpoint returns the ingestion keys for a workspace
|
||||
operationId: GetIngestionKeys
|
||||
parameters:
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: per_page
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -2597,6 +2606,19 @@ paths:
|
||||
deprecated: false
|
||||
description: This endpoint returns the ingestion keys for a workspace
|
||||
operationId: SearchIngestionKeys
|
||||
parameters:
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: per_page
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -2643,6 +2665,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
@@ -2697,6 +2720,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
@@ -2752,6 +2776,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
@@ -2918,6 +2943,7 @@ paths:
|
||||
parameters:
|
||||
- in: query
|
||||
name: metricName
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
@@ -3785,6 +3811,9 @@ components:
|
||||
type: string
|
||||
alertName:
|
||||
type: string
|
||||
required:
|
||||
- alertName
|
||||
- alertId
|
||||
type: object
|
||||
MetricsexplorertypesMetricAlertsResponse:
|
||||
properties:
|
||||
@@ -3793,6 +3822,8 @@ components:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricAlert'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- alerts
|
||||
type: object
|
||||
MetricsexplorertypesMetricAttribute:
|
||||
properties:
|
||||
@@ -3806,6 +3837,10 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- key
|
||||
- values
|
||||
- valueCount
|
||||
type: object
|
||||
MetricsexplorertypesMetricAttributesRequest:
|
||||
properties:
|
||||
@@ -3817,6 +3852,8 @@ components:
|
||||
start:
|
||||
nullable: true
|
||||
type: integer
|
||||
required:
|
||||
- metricName
|
||||
type: object
|
||||
MetricsexplorertypesMetricAttributesResponse:
|
||||
properties:
|
||||
@@ -3828,6 +3865,9 @@ components:
|
||||
totalKeys:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- attributes
|
||||
- totalKeys
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboard:
|
||||
properties:
|
||||
@@ -3839,6 +3879,11 @@ components:
|
||||
type: string
|
||||
widgetName:
|
||||
type: string
|
||||
required:
|
||||
- dashboardName
|
||||
- dashboardId
|
||||
- widgetId
|
||||
- widgetName
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboardsResponse:
|
||||
properties:
|
||||
@@ -3847,6 +3892,8 @@ components:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- dashboards
|
||||
type: object
|
||||
MetricsexplorertypesMetricHighlightsResponse:
|
||||
properties:
|
||||
@@ -3862,6 +3909,11 @@ components:
|
||||
totalTimeSeries:
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- dataPoints
|
||||
- lastReceived
|
||||
- totalTimeSeries
|
||||
- activeTimeSeries
|
||||
type: object
|
||||
MetricsexplorertypesMetricMetadata:
|
||||
properties:
|
||||
@@ -3870,11 +3922,27 @@ components:
|
||||
isMonotonic:
|
||||
type: boolean
|
||||
temporality:
|
||||
enum:
|
||||
- delta
|
||||
- cumulative
|
||||
- unspecified
|
||||
type: string
|
||||
type:
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
- description
|
||||
- type
|
||||
- unit
|
||||
- temporality
|
||||
- isMonotonic
|
||||
type: object
|
||||
MetricsexplorertypesStat:
|
||||
properties:
|
||||
@@ -3889,9 +3957,22 @@ components:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type:
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
- metricName
|
||||
- description
|
||||
- type
|
||||
- unit
|
||||
- timeseries
|
||||
- samples
|
||||
type: object
|
||||
MetricsexplorertypesStatsRequest:
|
||||
properties:
|
||||
@@ -3909,6 +3990,10 @@ components:
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
MetricsexplorertypesStatsResponse:
|
||||
properties:
|
||||
@@ -3920,6 +4005,9 @@ components:
|
||||
total:
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- metrics
|
||||
- total
|
||||
type: object
|
||||
MetricsexplorertypesTreemapEntry:
|
||||
properties:
|
||||
@@ -3931,6 +4019,10 @@ components:
|
||||
totalValue:
|
||||
minimum: 0
|
||||
type: integer
|
||||
required:
|
||||
- metricName
|
||||
- percentage
|
||||
- totalValue
|
||||
type: object
|
||||
MetricsexplorertypesTreemapRequest:
|
||||
properties:
|
||||
@@ -3942,10 +4034,18 @@ components:
|
||||
limit:
|
||||
type: integer
|
||||
mode:
|
||||
enum:
|
||||
- timeseries
|
||||
- samples
|
||||
type: string
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
- limit
|
||||
- mode
|
||||
type: object
|
||||
MetricsexplorertypesTreemapResponse:
|
||||
properties:
|
||||
@@ -3959,6 +4059,9 @@ components:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesTreemapEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- timeseries
|
||||
- samples
|
||||
type: object
|
||||
MetricsexplorertypesUpdateMetricMetadataRequest:
|
||||
properties:
|
||||
@@ -3969,11 +4072,28 @@ components:
|
||||
metricName:
|
||||
type: string
|
||||
temporality:
|
||||
enum:
|
||||
- delta
|
||||
- cumulative
|
||||
- unspecified
|
||||
type: string
|
||||
type:
|
||||
enum:
|
||||
- gauge
|
||||
- sum
|
||||
- histogram
|
||||
- summary
|
||||
- exponentialhistogram
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
required:
|
||||
- metricName
|
||||
- type
|
||||
- description
|
||||
- unit
|
||||
- temporality
|
||||
- isMonotonic
|
||||
type: object
|
||||
PreferencetypesPreference:
|
||||
properties:
|
||||
|
||||
@@ -79,7 +79,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.Name, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.Name, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -285,7 +285,7 @@ func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashb
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.Name, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -116,18 +116,18 @@ func (setter *setter) Patch(ctx context.Context, orgID valuer.UUID, role *rolety
|
||||
return setter.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
|
||||
}
|
||||
|
||||
func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||
func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||
_, err := setter.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||
}
|
||||
|
||||
additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions)
|
||||
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions)
|
||||
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ type GetAnomaliesResponse struct {
|
||||
//
|
||||
// ^ ^
|
||||
// | |
|
||||
// (rounded value for past peiod) + (seasonal growth)
|
||||
// (rounded value for past period) + (seasonal growth)
|
||||
//
|
||||
// score = abs(value - prediction) / stddev (current_season_query)
|
||||
type anomalyQueryParams struct {
|
||||
@@ -74,12 +74,12 @@ type anomalyQueryParams struct {
|
||||
// : For daily seasonality, this is the query range params for the (now-2d-5m, now-1d)
|
||||
// : For hourly seasonality, this is the query range params for the (now-2h-5m, now-1h)
|
||||
PastSeasonQuery *v3.QueryRangeParamsV3
|
||||
// Past2SeasonQuery is the query range params for past 2 seasonal period to the current season
|
||||
// Past2SeasonQuery is the query range params for past 2 seasonal periods to the current season
|
||||
// Example: For weekly seasonality, this is the query range params for the (now-3w-5m, now-2w)
|
||||
// : For daily seasonality, this is the query range params for the (now-3d-5m, now-2d)
|
||||
// : For hourly seasonality, this is the query range params for the (now-3h-5m, now-2h)
|
||||
Past2SeasonQuery *v3.QueryRangeParamsV3
|
||||
// Past3SeasonQuery is the query range params for past 3 seasonal period to the current season
|
||||
// Past3SeasonQuery is the query range params for past 3 seasonal periods to the current season
|
||||
// Example: For weekly seasonality, this is the query range params for the (now-4w-5m, now-3w)
|
||||
// : For daily seasonality, this is the query range params for the (now-4d-5m, now-3d)
|
||||
// : For hourly seasonality, this is the query range params for the (now-4h-5m, now-3h)
|
||||
|
||||
@@ -234,6 +234,11 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
}
|
||||
}
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
@@ -285,6 +290,11 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
|
||||
queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult)
|
||||
|
||||
hasData := len(queryResult.AnomalyScores) > 0
|
||||
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
|
||||
return ruletypes.Vector{*missingDataAlert}, nil
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
|
||||
268
ee/query-service/rules/anomaly_test.go
Normal file
268
ee/query-service/rules/anomaly_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/anomaly"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// mockAnomalyProvider is a mock implementation of anomaly.Provider for testing.
|
||||
// We need this because the anomaly provider makes 6 different queries for various
|
||||
// time periods (current, past period, current season, past season, past 2 seasons,
|
||||
// past 3 seasons), making it cumbersome to create mock data.
|
||||
type mockAnomalyProvider struct {
|
||||
responses []*anomaly.GetAnomaliesResponse
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.GetAnomaliesRequest) (*anomaly.GetAnomaliesResponse, error) {
|
||||
if m.callCount >= len(m.responses) {
|
||||
return &anomaly.GetAnomaliesResponse{Results: []*v3.Result{}}, nil
|
||||
}
|
||||
resp := m.responses[m.callCount]
|
||||
m.callCount++
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
||||
// Test basic AlertOnAbsent functionality (without AbsentFor grace period)
|
||||
|
||||
baseTime := time.Unix(1700000000, 0)
|
||||
evalWindow := 5 * time.Minute
|
||||
evalTime := baseTime.Add(5 * time.Minute)
|
||||
|
||||
target := 500.0
|
||||
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: ruletypes.Duration(evalWindow),
|
||||
Frequency: ruletypes.Duration(1 * time.Minute),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
description string
|
||||
alertOnAbsent bool
|
||||
expectAlerts int
|
||||
}{
|
||||
{
|
||||
description: "AlertOnAbsent=false",
|
||||
alertOnAbsent: false,
|
||||
expectAlerts: 0,
|
||||
},
|
||||
{
|
||||
description: "AlertOnAbsent=true",
|
||||
alertOnAbsent: true,
|
||||
expectAlerts: 1,
|
||||
},
|
||||
}
|
||||
|
||||
logger := instrumentationtest.New().Logger()
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
postableRule.RuleCondition.AlertOnAbsent = c.alertOnAbsent
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule(
|
||||
"test-anomaly-rule",
|
||||
valuer.GenerateUUID(),
|
||||
&postableRule,
|
||||
reader,
|
||||
nil,
|
||||
logger,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseNoData},
|
||||
}
|
||||
|
||||
alertsFound, err := rule.Eval(context.Background(), evalTime)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectAlerts, alertsFound)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
||||
// Test missing data alert with AbsentFor grace period
|
||||
// 1. Call Eval with data at time t1, to populate lastTimestampWithDatapoints
|
||||
// 2. Call Eval without data at time t2
|
||||
// 3. Alert fires only if t2 - t1 > AbsentFor
|
||||
|
||||
baseTime := time.Unix(1700000000, 0)
|
||||
evalWindow := 5 * time.Minute
|
||||
|
||||
// Set target higher than test data so regular threshold alerts don't fire
|
||||
target := 500.0
|
||||
|
||||
postableRule := ruletypes.PostableRule{
|
||||
AlertName: "Test anomaly no data with AbsentFor",
|
||||
AlertType: ruletypes.AlertTypeMetric,
|
||||
RuleType: RuleTypeAnomaly,
|
||||
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
|
||||
EvalWindow: ruletypes.Duration(evalWindow),
|
||||
Frequency: ruletypes.Duration(time.Minute),
|
||||
}},
|
||||
RuleCondition: &ruletypes.RuleCondition{
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
AlertOnAbsent: true,
|
||||
Target: &target,
|
||||
CompositeQuery: &v3.CompositeQuery{
|
||||
QueryType: v3.QueryTypeBuilder,
|
||||
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||
"A": {
|
||||
QueryName: "A",
|
||||
Expression: "A",
|
||||
DataSource: v3.DataSourceMetrics,
|
||||
Temporality: v3.Unspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
SelectedQuery: "A",
|
||||
Seasonality: "daily",
|
||||
Thresholds: &ruletypes.RuleThresholdData{
|
||||
Kind: ruletypes.BasicThresholdKind,
|
||||
Spec: ruletypes.BasicRuleThresholds{{
|
||||
Name: "Test anomaly no data with AbsentFor",
|
||||
TargetValue: &target,
|
||||
MatchType: ruletypes.AtleastOnce,
|
||||
CompareOp: ruletypes.ValueIsAbove,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
responseNoData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
description string
|
||||
absentFor uint64
|
||||
timeBetweenEvals time.Duration
|
||||
expectAlertOnEval2 int
|
||||
}{
|
||||
{
|
||||
description: "WithinGracePeriod",
|
||||
absentFor: 5,
|
||||
timeBetweenEvals: 4 * time.Minute,
|
||||
expectAlertOnEval2: 0,
|
||||
},
|
||||
{
|
||||
description: "AfterGracePeriod",
|
||||
absentFor: 5,
|
||||
timeBetweenEvals: 6 * time.Minute,
|
||||
expectAlertOnEval2: 1,
|
||||
},
|
||||
}
|
||||
|
||||
logger := instrumentationtest.New().Logger()
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.description, func(t *testing.T) {
|
||||
postableRule.RuleCondition.AbsentFor = c.absentFor
|
||||
|
||||
t1 := baseTime.Add(5 * time.Minute)
|
||||
t2 := t1.Add(c.timeBetweenEvals)
|
||||
|
||||
responseWithData := &anomaly.GetAnomaliesResponse{
|
||||
Results: []*v3.Result{
|
||||
{
|
||||
QueryName: "A",
|
||||
AnomalyScores: []*v3.Series{
|
||||
{
|
||||
Labels: map[string]string{"test": "label"},
|
||||
Points: []v3.Point{
|
||||
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
|
||||
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||
|
||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
rule.provider = &mockAnomalyProvider{
|
||||
responses: []*anomaly.GetAnomaliesResponse{responseWithData, responseNoData},
|
||||
}
|
||||
|
||||
alertsFound1, err := rule.Eval(context.Background(), t1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, alertsFound1, "First eval with data should not alert")
|
||||
|
||||
alertsFound2, err := rule.Eval(context.Background(), t2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectAlertOnEval2, alertsFound2)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ module.exports = {
|
||||
curly: 'error', // Requires curly braces for all control statements
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
|
||||
// TODO: Change this to error in May 2026
|
||||
'max-params': ['warn', 3], // a function can have max 3 params after which it should become an object
|
||||
|
||||
// TypeScript rules
|
||||
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
|
||||
@@ -116,7 +118,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }], // Prevents importing packages not in package.json
|
||||
// 'import/no-cycle': 'warn', // TODO: Enable later to detect circular dependencies
|
||||
'import/no-cycle': 'warn', // Warns about circular dependencies
|
||||
|
||||
// Import sorting rules
|
||||
'simple-import-sort/imports': [
|
||||
@@ -146,6 +148,19 @@ module.exports = {
|
||||
'sonarjs/no-duplicate-string': 'off', // Disabled - can be noisy (enable periodically to check)
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/*.test.{js,jsx,ts,tsx}',
|
||||
'**/*.spec.{js,jsx,ts,tsx}',
|
||||
'**/__tests__/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
// Tests often have intentional duplication and complexity - disable SonarJS rules
|
||||
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
|
||||
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
|
||||
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/api/generated/**/*.ts'],
|
||||
rules: {
|
||||
@@ -153,7 +168,6 @@ module.exports = {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion.
|
||||
|
||||
### Export Style
|
||||
|
||||
- **React components** (`src/components/`, `src/container/`, `src/pages/`): Prefer **default exports** for the main component in each file
|
||||
- **Utilities, hooks, APIs, types, constants** (`src/utils/`, `src/hooks/`, `src/api/`, `src/lib/`, `src/types/`, `src/constants/`): Prefer **named exports** for better tree-shaking and explicit imports
|
||||
|
||||
### React and Components
|
||||
|
||||
- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability.
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
@@ -105,6 +105,7 @@
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"i18next-http-backend": "^1.3.2",
|
||||
"immer": "11.1.3",
|
||||
"jest": "^27.5.1",
|
||||
"js-base64": "^3.7.2",
|
||||
"less": "^4.1.2",
|
||||
|
||||
@@ -39,7 +39,7 @@ type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* This endpoints deletes the public sharing config and disables the public sharing of a dashboard
|
||||
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
|
||||
* @summary Delete public dashboard
|
||||
*/
|
||||
export const deletePublicDashboard = ({
|
||||
@@ -118,7 +118,7 @@ export const useDeletePublicDashboard = <
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoints returns public sharing config for a dashboard
|
||||
* This endpoint returns public sharing config for a dashboard
|
||||
* @summary Get public dashboard
|
||||
*/
|
||||
export const getPublicDashboard = (
|
||||
@@ -222,7 +222,7 @@ export const invalidateGetPublicDashboard = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoints creates public sharing config and enables public sharing of the dashboard
|
||||
* This endpoint creates public sharing config and enables public sharing of the dashboard
|
||||
* @summary Create public dashboard
|
||||
*/
|
||||
export const createPublicDashboard = (
|
||||
@@ -321,7 +321,7 @@ export const useCreatePublicDashboard = <
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoints updates the public sharing config for a dashboard
|
||||
* This endpoint updates the public sharing config for a dashboard
|
||||
* @summary Update public dashboard
|
||||
*/
|
||||
export const updatePublicDashboard = (
|
||||
@@ -418,7 +418,7 @@ export const useUpdatePublicDashboard = <
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoints returns the sanitized dashboard data for public access
|
||||
* This endpoint returns the sanitized dashboard data for public access
|
||||
* @summary Get public dashboard data
|
||||
*/
|
||||
export const getPublicDashboardData = (
|
||||
|
||||
@@ -28,8 +28,10 @@ import type {
|
||||
GatewaytypesPostableIngestionKeyLimitDTO,
|
||||
GatewaytypesUpdatableIngestionKeyLimitDTO,
|
||||
GetIngestionKeys200,
|
||||
GetIngestionKeysParams,
|
||||
RenderErrorResponseDTO,
|
||||
SearchIngestionKeys200,
|
||||
SearchIngestionKeysParams,
|
||||
UpdateIngestionKeyLimitPathParameters,
|
||||
UpdateIngestionKeyPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -42,35 +44,44 @@ type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
* This endpoint returns the ingestion keys for a workspace
|
||||
* @summary Get ingestion keys for workspace
|
||||
*/
|
||||
export const getIngestionKeys = (signal?: AbortSignal) => {
|
||||
export const getIngestionKeys = (
|
||||
params?: GetIngestionKeysParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetIngestionKeys200>({
|
||||
url: `/api/v2/gateway/ingestion_keys`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetIngestionKeysQueryKey = () => {
|
||||
return ['getIngestionKeys'] as const;
|
||||
export const getGetIngestionKeysQueryKey = (
|
||||
params?: GetIngestionKeysParams,
|
||||
) => {
|
||||
return ['getIngestionKeys', ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getGetIngestionKeysQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
>(
|
||||
params?: GetIngestionKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetIngestionKeysQueryKey();
|
||||
const queryKey = queryOptions?.queryKey ?? getGetIngestionKeysQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getIngestionKeys>>> = ({
|
||||
signal,
|
||||
}) => getIngestionKeys(signal);
|
||||
}) => getIngestionKeys(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
@@ -91,14 +102,17 @@ export type GetIngestionKeysQueryError = RenderErrorResponseDTO;
|
||||
export function useGetIngestionKeys<
|
||||
TData = Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetIngestionKeysQueryOptions(options);
|
||||
>(
|
||||
params?: GetIngestionKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetIngestionKeysQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
@@ -114,10 +128,11 @@ export function useGetIngestionKeys<
|
||||
*/
|
||||
export const invalidateGetIngestionKeys = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetIngestionKeysParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetIngestionKeysQueryKey() },
|
||||
{ queryKey: getGetIngestionKeysQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -662,35 +677,45 @@ export const useUpdateIngestionKeyLimit = <
|
||||
* This endpoint returns the ingestion keys for a workspace
|
||||
* @summary Search ingestion keys for workspace
|
||||
*/
|
||||
export const searchIngestionKeys = (signal?: AbortSignal) => {
|
||||
export const searchIngestionKeys = (
|
||||
params?: SearchIngestionKeysParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<SearchIngestionKeys200>({
|
||||
url: `/api/v2/gateway/ingestion_keys/search`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getSearchIngestionKeysQueryKey = () => {
|
||||
return ['searchIngestionKeys'] as const;
|
||||
export const getSearchIngestionKeysQueryKey = (
|
||||
params?: SearchIngestionKeysParams,
|
||||
) => {
|
||||
return ['searchIngestionKeys', ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getSearchIngestionKeysQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
>(
|
||||
params?: SearchIngestionKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getSearchIngestionKeysQueryKey();
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getSearchIngestionKeysQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof searchIngestionKeys>>
|
||||
> = ({ signal }) => searchIngestionKeys(signal);
|
||||
> = ({ signal }) => searchIngestionKeys(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
@@ -711,14 +736,17 @@ export type SearchIngestionKeysQueryError = RenderErrorResponseDTO;
|
||||
export function useSearchIngestionKeys<
|
||||
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getSearchIngestionKeysQueryOptions(options);
|
||||
>(
|
||||
params?: SearchIngestionKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof searchIngestionKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getSearchIngestionKeysQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
@@ -734,10 +762,11 @@ export function useSearchIngestionKeys<
|
||||
*/
|
||||
export const invalidateSearchIngestionKeys = async (
|
||||
queryClient: QueryClient,
|
||||
params?: SearchIngestionKeysParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getSearchIngestionKeysQueryKey() },
|
||||
{ queryKey: getSearchIngestionKeysQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* This endpoints returns global config
|
||||
* This endpoint returns global config
|
||||
* @summary Get global config
|
||||
*/
|
||||
export const getGlobalConfig = (signal?: AbortSignal) => {
|
||||
|
||||
@@ -47,7 +47,7 @@ type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
* @summary Get metric alerts
|
||||
*/
|
||||
export const getMetricAlerts = (
|
||||
params?: GetMetricAlertsParams,
|
||||
params: GetMetricAlertsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricAlerts200>({
|
||||
@@ -66,7 +66,7 @@ export const getGetMetricAlertsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricAlertsParams,
|
||||
params: GetMetricAlertsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
@@ -103,7 +103,7 @@ export function useGetMetricAlerts<
|
||||
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricAlertsParams,
|
||||
params: GetMetricAlertsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricAlerts>>,
|
||||
@@ -128,7 +128,7 @@ export function useGetMetricAlerts<
|
||||
*/
|
||||
export const invalidateGetMetricAlerts = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetMetricAlertsParams,
|
||||
params: GetMetricAlertsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
@@ -144,7 +144,7 @@ export const invalidateGetMetricAlerts = async (
|
||||
* @summary Get metric dashboards
|
||||
*/
|
||||
export const getMetricDashboards = (
|
||||
params?: GetMetricDashboardsParams,
|
||||
params: GetMetricDashboardsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricDashboards200>({
|
||||
@@ -165,7 +165,7 @@ export const getGetMetricDashboardsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricDashboardsParams,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
@@ -203,7 +203,7 @@ export function useGetMetricDashboards<
|
||||
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricDashboardsParams,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricDashboards>>,
|
||||
@@ -228,7 +228,7 @@ export function useGetMetricDashboards<
|
||||
*/
|
||||
export const invalidateGetMetricDashboards = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetMetricDashboardsParams,
|
||||
params: GetMetricDashboardsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
@@ -244,7 +244,7 @@ export const invalidateGetMetricDashboards = async (
|
||||
* @summary Get metric highlights
|
||||
*/
|
||||
export const getMetricHighlights = (
|
||||
params?: GetMetricHighlightsParams,
|
||||
params: GetMetricHighlightsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricHighlights200>({
|
||||
@@ -265,7 +265,7 @@ export const getGetMetricHighlightsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricHighlightsParams,
|
||||
params: GetMetricHighlightsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
@@ -303,7 +303,7 @@ export function useGetMetricHighlights<
|
||||
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricHighlightsParams,
|
||||
params: GetMetricHighlightsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricHighlights>>,
|
||||
@@ -328,7 +328,7 @@ export function useGetMetricHighlights<
|
||||
*/
|
||||
export const invalidateGetMetricHighlights = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetMetricHighlightsParams,
|
||||
params: GetMetricHighlightsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
@@ -526,7 +526,7 @@ export const useGetMetricAttributes = <
|
||||
* @summary Get metric metadata
|
||||
*/
|
||||
export const getMetricMetadata = (
|
||||
params?: GetMetricMetadataParams,
|
||||
params: GetMetricMetadataParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetMetricMetadata200>({
|
||||
@@ -547,7 +547,7 @@ export const getGetMetricMetadataQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricMetadataParams,
|
||||
params: GetMetricMetadataParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
@@ -585,7 +585,7 @@ export function useGetMetricMetadata<
|
||||
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
params?: GetMetricMetadataParams,
|
||||
params: GetMetricMetadataParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getMetricMetadata>>,
|
||||
@@ -610,7 +610,7 @@ export function useGetMetricMetadata<
|
||||
*/
|
||||
export const invalidateGetMetricMetadata = async (
|
||||
queryClient: QueryClient,
|
||||
params?: GetMetricMetadataParams,
|
||||
params: GetMetricMetadataParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
|
||||
433
frontend/src/api/generated/services/role/index.ts
Normal file
433
frontend/src/api/generated/services/role/index.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../index';
|
||||
import type {
|
||||
CreateRole201,
|
||||
DeleteRolePathParameters,
|
||||
GetRole200,
|
||||
GetRolePathParameters,
|
||||
ListRoles200,
|
||||
PatchRolePathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* This endpoint lists all roles
|
||||
* @summary List roles
|
||||
*/
|
||||
export const listRoles = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<ListRoles200>({
|
||||
url: `/api/v1/roles`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListRolesQueryKey = () => {
|
||||
return ['listRoles'] as const;
|
||||
};
|
||||
|
||||
export const getListRolesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listRoles>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listRoles>>, TError, TData>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListRolesQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRoles>>> = ({
|
||||
signal,
|
||||
}) => listRoles(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listRoles>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListRolesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listRoles>>
|
||||
>;
|
||||
export type ListRolesQueryError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary List roles
|
||||
*/
|
||||
|
||||
export function useListRoles<
|
||||
TData = Awaited<ReturnType<typeof listRoles>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof listRoles>>, TError, TData>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListRolesQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List roles
|
||||
*/
|
||||
export const invalidateListRoles = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListRolesQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a role
|
||||
* @summary Create role
|
||||
*/
|
||||
export const createRole = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<CreateRole201>({
|
||||
url: `/api/v1/roles`,
|
||||
method: 'POST',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateRoleMutationOptions = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createRole'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
void
|
||||
> = () => {
|
||||
return createRole();
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createRole>>
|
||||
>;
|
||||
|
||||
export type CreateRoleMutationError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Create role
|
||||
*/
|
||||
export const useCreateRole = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createRole>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateRoleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint deletes a role
|
||||
* @summary Delete role
|
||||
*/
|
||||
export const deleteRole = ({ id }: DeleteRolePathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/roles/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteRoleMutationOptions = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteRole'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
{ pathParams: DeleteRolePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteRole(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteRole>>
|
||||
>;
|
||||
|
||||
export type DeleteRoleMutationError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Delete role
|
||||
*/
|
||||
export const useDeleteRole = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteRole>>,
|
||||
TError,
|
||||
{ pathParams: DeleteRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteRoleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoint gets a role
|
||||
* @summary Get role
|
||||
*/
|
||||
export const getRole = (
|
||||
{ id }: GetRolePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRole200>({
|
||||
url: `/api/v1/roles/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRoleQueryKey = ({ id }: GetRolePathParameters) => {
|
||||
return ['getRole'] as const;
|
||||
};
|
||||
|
||||
export const getGetRoleQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRole>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
{ id }: GetRolePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetRoleQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getRole>>> = ({
|
||||
signal,
|
||||
}) => getRole({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetRoleQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRole>>
|
||||
>;
|
||||
export type GetRoleQueryError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Get role
|
||||
*/
|
||||
|
||||
export function useGetRole<
|
||||
TData = Awaited<ReturnType<typeof getRole>>,
|
||||
TError = RenderErrorResponseDTO
|
||||
>(
|
||||
{ id }: GetRolePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof getRole>>, TError, TData>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRoleQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get role
|
||||
*/
|
||||
export const invalidateGetRole = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRolePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRoleQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint patches a role
|
||||
* @summary Patch role
|
||||
*/
|
||||
export const patchRole = ({ id }: PatchRolePathParameters) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/roles/${id}`,
|
||||
method: 'PATCH',
|
||||
});
|
||||
};
|
||||
|
||||
export const getPatchRoleMutationOptions = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchRole>>,
|
||||
TError,
|
||||
{ pathParams: PatchRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchRole>>,
|
||||
TError,
|
||||
{ pathParams: PatchRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['patchRole'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof patchRole>>,
|
||||
{ pathParams: PatchRolePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return patchRole(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type PatchRoleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof patchRole>>
|
||||
>;
|
||||
|
||||
export type PatchRoleMutationError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Patch role
|
||||
*/
|
||||
export const usePatchRole = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof patchRole>>,
|
||||
TError,
|
||||
{ pathParams: PatchRolePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof patchRole>>,
|
||||
TError,
|
||||
{ pathParams: PatchRolePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getPatchRoleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -650,11 +650,11 @@ export interface MetricsexplorertypesMetricAlertDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alertId?: string;
|
||||
alertId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
alertName?: string;
|
||||
alertName: string;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricAlertsResponseDTO {
|
||||
@@ -662,24 +662,24 @@ export interface MetricsexplorertypesMetricAlertsResponseDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
alerts?: MetricsexplorertypesMetricAlertDTO[] | null;
|
||||
alerts: MetricsexplorertypesMetricAlertDTO[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricAttributeDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key?: string;
|
||||
key: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
valueCount?: number;
|
||||
valueCount: number;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
values?: string[] | null;
|
||||
values: string[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricAttributesRequestDTO {
|
||||
@@ -691,7 +691,7 @@ export interface MetricsexplorertypesMetricAttributesRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @nullable true
|
||||
@@ -704,31 +704,31 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
attributes?: MetricsexplorertypesMetricAttributeDTO[] | null;
|
||||
attributes: MetricsexplorertypesMetricAttributeDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
totalKeys?: number;
|
||||
totalKeys: number;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricDashboardDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
dashboardId?: string;
|
||||
dashboardId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
dashboardName?: string;
|
||||
dashboardName: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
widgetId?: string;
|
||||
widgetId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
widgetName?: string;
|
||||
widgetName: string;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
|
||||
@@ -736,7 +736,7 @@ export interface MetricsexplorertypesMetricDashboardsResponseDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
dashboards?: MetricsexplorertypesMetricDashboardDTO[] | null;
|
||||
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
|
||||
@@ -744,74 +744,96 @@ export interface MetricsexplorertypesMetricHighlightsResponseDTO {
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
activeTimeSeries?: number;
|
||||
activeTimeSeries: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
dataPoints?: number;
|
||||
dataPoints: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
lastReceived?: number;
|
||||
lastReceived: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalTimeSeries?: number;
|
||||
totalTimeSeries: number;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesMetricMetadataDTOTemporality {
|
||||
delta = 'delta',
|
||||
cumulative = 'cumulative',
|
||||
unspecified = 'unspecified',
|
||||
}
|
||||
export enum MetricsexplorertypesMetricMetadataDTOType {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface MetricsexplorertypesMetricMetadataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
description: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isMonotonic?: boolean;
|
||||
isMonotonic: boolean;
|
||||
/**
|
||||
* @enum delta,cumulative,unspecified
|
||||
* @type string
|
||||
*/
|
||||
temporality: MetricsexplorertypesMetricMetadataDTOTemporality;
|
||||
/**
|
||||
* @enum gauge,sum,histogram,summary,exponentialhistogram
|
||||
* @type string
|
||||
*/
|
||||
type: MetricsexplorertypesMetricMetadataDTOType;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
temporality?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesStatDTOType {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface MetricsexplorertypesStatDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
description: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
samples?: number;
|
||||
samples: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timeseries?: number;
|
||||
timeseries: number;
|
||||
/**
|
||||
* @enum gauge,sum,histogram,summary,exponentialhistogram
|
||||
* @type string
|
||||
*/
|
||||
type: MetricsexplorertypesStatDTOType;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesStatsRequestDTO {
|
||||
@@ -819,12 +841,12 @@ export interface MetricsexplorertypesStatsRequestDTO {
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end?: number;
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit?: number;
|
||||
limit: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
@@ -834,7 +856,7 @@ export interface MetricsexplorertypesStatsRequestDTO {
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
start?: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesStatsResponseDTO {
|
||||
@@ -842,51 +864,56 @@ export interface MetricsexplorertypesStatsResponseDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
metrics?: MetricsexplorertypesStatDTO[] | null;
|
||||
metrics: MetricsexplorertypesStatDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
total?: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesTreemapEntryDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
percentage?: number;
|
||||
percentage: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalValue?: number;
|
||||
totalValue: number;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesTreemapRequestDTOMode {
|
||||
timeseries = 'timeseries',
|
||||
samples = 'samples',
|
||||
}
|
||||
export interface MetricsexplorertypesTreemapRequestDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end?: number;
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit?: number;
|
||||
limit: number;
|
||||
/**
|
||||
* @enum timeseries,samples
|
||||
* @type string
|
||||
*/
|
||||
mode?: string;
|
||||
mode: MetricsexplorertypesTreemapRequestDTOMode;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
start?: number;
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesTreemapResponseDTO {
|
||||
@@ -894,39 +921,53 @@ export interface MetricsexplorertypesTreemapResponseDTO {
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
samples?: MetricsexplorertypesTreemapEntryDTO[] | null;
|
||||
samples: MetricsexplorertypesTreemapEntryDTO[] | null;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
timeseries?: MetricsexplorertypesTreemapEntryDTO[] | null;
|
||||
timeseries: MetricsexplorertypesTreemapEntryDTO[] | null;
|
||||
}
|
||||
|
||||
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality {
|
||||
delta = 'delta',
|
||||
cumulative = 'cumulative',
|
||||
unspecified = 'unspecified',
|
||||
}
|
||||
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOType {
|
||||
gauge = 'gauge',
|
||||
sum = 'sum',
|
||||
histogram = 'histogram',
|
||||
summary = 'summary',
|
||||
exponentialhistogram = 'exponentialhistogram',
|
||||
}
|
||||
export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
description: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isMonotonic?: boolean;
|
||||
isMonotonic: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
/**
|
||||
* @enum delta,cumulative,unspecified
|
||||
* @type string
|
||||
*/
|
||||
temporality: MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality;
|
||||
/**
|
||||
* @enum gauge,sum,histogram,summary,exponentialhistogram
|
||||
* @type string
|
||||
*/
|
||||
type: MetricsexplorertypesUpdateMetricMetadataRequestDTOType;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
temporality?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface PreferencetypesPreferenceDTO {
|
||||
@@ -1110,6 +1151,39 @@ export interface RenderErrorResponseDTO {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface RoletypesRoleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface TypesChangePasswordRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -1310,6 +1384,21 @@ export interface TypesPostableAcceptInviteDTO {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableForgotPasswordDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
frontendBaseURL?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId?: string;
|
||||
}
|
||||
|
||||
export interface TypesPostableInviteDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -1341,6 +1430,11 @@ export interface TypesPostableResetPasswordDTO {
|
||||
}
|
||||
|
||||
export interface TypesResetPasswordTokenDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
expiresAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -1682,6 +1776,42 @@ export type GetPublicDashboardWidgetQueryRange200 = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type ListRoles200 = {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
data?: RoletypesRoleDTO[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type CreateRole201 = {
|
||||
data?: TypesIdentifiableDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type DeleteRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetRole200 = {
|
||||
data?: RoletypesRoleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type PatchRolePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type ListUsers200 = {
|
||||
/**
|
||||
* @type array
|
||||
@@ -1762,6 +1892,19 @@ export type GetFeatures200 = {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type GetIngestionKeysParams = {
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
per_page?: number;
|
||||
};
|
||||
|
||||
export type GetIngestionKeys200 = {
|
||||
data?: GatewaytypesGettableIngestionKeysDTO;
|
||||
/**
|
||||
@@ -1801,6 +1944,24 @@ export type DeleteIngestionKeyLimitPathParameters = {
|
||||
export type UpdateIngestionKeyLimitPathParameters = {
|
||||
limitId: string;
|
||||
};
|
||||
export type SearchIngestionKeysParams = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
page?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
per_page?: number;
|
||||
};
|
||||
|
||||
export type SearchIngestionKeys200 = {
|
||||
data?: GatewaytypesGettableIngestionKeysDTO;
|
||||
/**
|
||||
@@ -1814,7 +1975,7 @@ export type GetMetricAlertsParams = {
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricAlerts200 = {
|
||||
@@ -1830,7 +1991,7 @@ export type GetMetricDashboardsParams = {
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricDashboards200 = {
|
||||
@@ -1846,7 +2007,7 @@ export type GetMetricHighlightsParams = {
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricHighlights200 = {
|
||||
@@ -1873,7 +2034,7 @@ export type GetMetricMetadataParams = {
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
metricName?: string;
|
||||
metricName: string;
|
||||
};
|
||||
|
||||
export type GetMetricMetadata200 = {
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
TypesChangePasswordRequestDTO,
|
||||
TypesPostableAcceptInviteDTO,
|
||||
TypesPostableAPIKeyDTO,
|
||||
TypesPostableForgotPasswordDTO,
|
||||
TypesPostableInviteDTO,
|
||||
TypesPostableResetPasswordDTO,
|
||||
TypesStorableAPIKeyDTO,
|
||||
@@ -1567,3 +1568,87 @@ export const invalidateGetMyUser = async (
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint initiates the forgot password flow by sending a reset password email
|
||||
* @summary Forgot password
|
||||
*/
|
||||
export const forgotPassword = (
|
||||
typesPostableForgotPasswordDTO: TypesPostableForgotPasswordDTO,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v2/factor_password/forgot`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: typesPostableForgotPasswordDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getForgotPasswordMutationOptions = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof forgotPassword>>,
|
||||
TError,
|
||||
{ data: TypesPostableForgotPasswordDTO },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof forgotPassword>>,
|
||||
TError,
|
||||
{ data: TypesPostableForgotPasswordDTO },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['forgotPassword'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof forgotPassword>>,
|
||||
{ data: TypesPostableForgotPasswordDTO }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return forgotPassword(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ForgotPasswordMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof forgotPassword>>
|
||||
>;
|
||||
export type ForgotPasswordMutationBody = TypesPostableForgotPasswordDTO;
|
||||
export type ForgotPasswordMutationError = RenderErrorResponseDTO;
|
||||
|
||||
/**
|
||||
* @summary Forgot password
|
||||
*/
|
||||
export const useForgotPassword = <
|
||||
TError = RenderErrorResponseDTO,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof forgotPassword>>,
|
||||
TError,
|
||||
{ data: TypesPostableForgotPasswordDTO },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof forgotPassword>>,
|
||||
TError,
|
||||
{ data: TypesPostableForgotPasswordDTO },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getForgotPasswordMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
background: var(--bg-ink-400, #121317);
|
||||
border: 1px solid var(--bg-ink-200, #23262e);
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-ink-200);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
color: var(--text-neutral-dark-100);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
.auth-footer-link-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-neutral-dark-50, #eceef2);
|
||||
color: var(--text-neutral-dark-50);
|
||||
}
|
||||
|
||||
.auth-footer-link-status {
|
||||
@@ -84,14 +84,14 @@
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-ink-200, #23262e);
|
||||
background: var(--bg-ink-200);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-footer-content {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
background: var(--bg-base-white);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@@ -102,14 +102,14 @@
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
color: var(--text-neutral-light-200);
|
||||
}
|
||||
|
||||
.auth-footer-link-icon {
|
||||
color: var(--text-neutral-light-100, #62636c);
|
||||
color: var(--text-neutral-light-100);
|
||||
}
|
||||
|
||||
.auth-footer-separator {
|
||||
background: var(--bg-vanilla-300, #e9e9e9);
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
font-size: 15.4px;
|
||||
font-weight: 500;
|
||||
line-height: 17.5px;
|
||||
color: var(--text-neutral-dark-50, #eceef2);
|
||||
color: var(--text-neutral-dark-50);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-ink-400, #121317);
|
||||
background: var(--bg-ink-400);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
@@ -52,13 +52,13 @@
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
color: var(--text-neutral-dark-100);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
color: var(--text-neutral-dark-100);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -68,15 +68,15 @@
|
||||
|
||||
.lightMode {
|
||||
.auth-header-logo-text {
|
||||
color: var(--text-neutral-light-100, #62636c);
|
||||
color: var(--text-neutral-light-100);
|
||||
}
|
||||
|
||||
.auth-header-help-button {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
background: var(--bg-vanilla-200);
|
||||
|
||||
span,
|
||||
svg {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
color: var(--text-neutral-light-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background: var(--bg-neutral-dark-1000, #0a0c10);
|
||||
background: var(--bg-neutral-dark-1000);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
.bg-dot-pattern {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
var(--bg-neutral-dark-50, #eceef2) 1px,
|
||||
var(--bg-neutral-dark-50) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 12px 12px;
|
||||
@@ -85,8 +85,8 @@
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-ink-200, #23262e) 0px,
|
||||
var(--bg-ink-200, #23262e) 4px,
|
||||
var(--bg-ink-200) 0px,
|
||||
var(--bg-ink-200) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
.lightMode {
|
||||
.auth-page-wrapper {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
background: var(--bg-base-white);
|
||||
}
|
||||
|
||||
.bg-dot-pattern {
|
||||
@@ -172,8 +172,8 @@
|
||||
.auth-page-line-right {
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300, #e9e9e9) 0px,
|
||||
var(--bg-vanilla-300, #e9e9e9) 4px,
|
||||
var(--bg-vanilla-300) 0px,
|
||||
var(--bg-vanilla-300) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
|
||||
import { clearPreviousQuery } from './QueryV2/previousQuery.utils';
|
||||
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
|
||||
import { QueryV2 } from './QueryV2/QueryV2';
|
||||
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
|
||||
@@ -24,6 +25,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
version,
|
||||
onSignalSourceChange,
|
||||
signalSourceChangeEnabled = false,
|
||||
savePreviousQuery = false,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
@@ -61,6 +63,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// always clear on mount and unmount to avoid stale data
|
||||
clearPreviousQuery();
|
||||
return (): void => {
|
||||
clearPreviousQuery();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isMultiQueryAllowed = useMemo(
|
||||
() => !isListViewPanel || showTraceOperator,
|
||||
[showTraceOperator, isListViewPanel],
|
||||
@@ -200,6 +210,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={1}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
) : (
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
@@ -222,6 +233,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={currentQuery.builder.queryData.length}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,13 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
import {
|
||||
getPreviousQueryFromKey,
|
||||
getQueryKey,
|
||||
removeKeyFromPreviousQuery,
|
||||
saveAsPreviousQuery,
|
||||
} from '../previousQuery.utils';
|
||||
|
||||
import './MetricsSelect.styles.scss';
|
||||
|
||||
export const SOURCE_OPTIONS: SelectOption<string, string>[] = [
|
||||
@@ -27,6 +34,7 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
signalSource,
|
||||
onSignalSourceChange,
|
||||
signalSourceChangeEnabled = false,
|
||||
savePreviousQuery = false,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
index: number;
|
||||
@@ -34,6 +42,7 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
signalSource: 'meter' | '';
|
||||
onSignalSourceChange: (value: string) => void;
|
||||
signalSourceChangeEnabled: boolean;
|
||||
savePreviousQuery: boolean;
|
||||
}): JSX.Element {
|
||||
const [attributeKeys, setAttributeKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
|
||||
@@ -50,7 +59,11 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
[handleChangeAggregatorAttribute, attributeKeys],
|
||||
);
|
||||
|
||||
const { updateAllQueriesOperators, handleSetQueryData } = useQueryBuilder();
|
||||
const {
|
||||
updateAllQueriesOperators,
|
||||
handleSetQueryData,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const source = useMemo(
|
||||
() => (signalSource === 'meter' ? 'meter' : 'metrics'),
|
||||
@@ -79,22 +92,63 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const getDefaultQueryFromSource = useCallback(
|
||||
(selectedSource: string): IBuilderQuery => {
|
||||
const isMeter = selectedSource === 'meter';
|
||||
const baseQuery = isMeter
|
||||
? defaultMeterQuery.builder.queryData[0]
|
||||
: defaultMetricsQuery.builder.queryData[0];
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
source: isMeter ? 'meter' : '',
|
||||
queryName: query.queryName,
|
||||
};
|
||||
},
|
||||
[defaultMeterQuery, defaultMetricsQuery, query.queryName],
|
||||
);
|
||||
|
||||
const handleSignalSourceChange = (value: string): void => {
|
||||
let newQueryData: IBuilderQuery;
|
||||
|
||||
if (savePreviousQuery) {
|
||||
const queryName = query.queryName || '';
|
||||
const dataSource = query.dataSource || '';
|
||||
const currSignalSource = query.source ?? '';
|
||||
const newSignalSource = value === 'meter' ? 'meter' : '';
|
||||
|
||||
const currQueryKey = getQueryKey({
|
||||
queryName: queryName,
|
||||
dataSource: dataSource,
|
||||
signalSource: currSignalSource,
|
||||
panelType: panelType || '',
|
||||
});
|
||||
|
||||
// save the current query key in session storage
|
||||
saveAsPreviousQuery(currQueryKey, query);
|
||||
|
||||
const newQueryKey = getQueryKey({
|
||||
queryName: queryName,
|
||||
dataSource: dataSource,
|
||||
signalSource: newSignalSource,
|
||||
panelType: panelType || '',
|
||||
});
|
||||
const savedQuery: IBuilderQuery | null = getPreviousQueryFromKey(
|
||||
newQueryKey,
|
||||
);
|
||||
|
||||
// remove the new query key from session storage
|
||||
removeKeyFromPreviousQuery(newQueryKey);
|
||||
|
||||
newQueryData = savedQuery
|
||||
? savedQuery
|
||||
: getDefaultQueryFromSource(newSignalSource);
|
||||
} else {
|
||||
newQueryData = getDefaultQueryFromSource(value);
|
||||
}
|
||||
|
||||
onSignalSourceChange(value);
|
||||
handleSetQueryData(
|
||||
index,
|
||||
value === 'meter'
|
||||
? {
|
||||
...defaultMeterQuery.builder.queryData[0],
|
||||
source: 'meter',
|
||||
queryName: query.queryName,
|
||||
}
|
||||
: {
|
||||
...defaultMetricsQuery.builder.queryData[0],
|
||||
source: '',
|
||||
queryName: query.queryName,
|
||||
},
|
||||
);
|
||||
handleSetQueryData(index, newQueryData);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,17 +28,16 @@ import {
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
import { Info, TriangleAlert } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
IDetailedError,
|
||||
IQueryContext,
|
||||
IValidationResult,
|
||||
} from 'types/antlrQueryTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -207,14 +206,9 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const dynamicVariables = useMemo(
|
||||
() =>
|
||||
Object.values(selectedDashboard?.data?.variables || {})?.filter(
|
||||
(variable: IDashboardVariable) => variable.type === 'DYNAMIC',
|
||||
),
|
||||
[selectedDashboard],
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
@@ -1069,7 +1063,7 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
// Add dynamic variables suggestions for the current key
|
||||
const variableName = dynamicVariables?.find(
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
||||
)?.name;
|
||||
|
||||
|
||||
@@ -42,10 +42,12 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
onSignalSourceChange,
|
||||
signalSourceChangeEnabled = false,
|
||||
queriesCount = 1,
|
||||
savePreviousQuery = false,
|
||||
}: QueryProps & {
|
||||
onSignalSourceChange: (value: string) => void;
|
||||
signalSourceChangeEnabled: boolean;
|
||||
queriesCount: number;
|
||||
savePreviousQuery: boolean;
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
): JSX.Element {
|
||||
@@ -67,6 +69,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
filterConfigs,
|
||||
isListViewPanel,
|
||||
entityVersion: version,
|
||||
savePreviousQuery,
|
||||
});
|
||||
|
||||
const handleToggleDisableQuery = useCallback(() => {
|
||||
@@ -234,6 +237,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
signalSource={signalSource as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,9 +11,14 @@ import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { UseQueryOperations } from 'types/common/operations.types';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
import {
|
||||
DataSource,
|
||||
QueryBuilderContextType,
|
||||
QueryFunctionsTypes,
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
@@ -45,12 +50,17 @@ const mockedUseQueryOperations = jest.mocked(
|
||||
|
||||
describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
let handleRunQueryMock: jest.MockedFunction<() => void>;
|
||||
let handleQueryFunctionsUpdatesMock: jest.MockedFunction<() => void>;
|
||||
let baseQBContext: QueryBuilderContextType;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockCloneQuery = jest.fn() as jest.MockedFunction<
|
||||
(type: string, q: IBuilderQuery) => void
|
||||
>;
|
||||
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
|
||||
handleQueryFunctionsUpdatesMock = jest.fn() as jest.MockedFunction<
|
||||
() => void
|
||||
>;
|
||||
const baseQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.LOGS,
|
||||
@@ -91,7 +101,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
const updateQueriesData: QueryBuilderContextType['updateQueriesData'] = (q) =>
|
||||
q;
|
||||
|
||||
mockedUseQueryBuilder.mockReturnValue(({
|
||||
const baseContext = ({
|
||||
currentQuery: currentQueryObj,
|
||||
stagedQuery: null,
|
||||
lastUsedQuery: null,
|
||||
@@ -124,7 +134,10 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
} as unknown) as QueryBuilderContextType);
|
||||
} as unknown) as QueryBuilderContextType;
|
||||
|
||||
baseQBContext = baseContext;
|
||||
mockedUseQueryBuilder.mockReturnValue(baseQBContext);
|
||||
|
||||
mockedUseQueryOperations.mockReturnValue({
|
||||
isTracePanelType: false,
|
||||
@@ -139,7 +152,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
handleDeleteQuery: jest.fn(),
|
||||
handleChangeQueryData: (jest.fn() as unknown) as ReturnType<UseQueryOperations>['handleChangeQueryData'],
|
||||
handleChangeFormulaData: jest.fn(),
|
||||
handleQueryFunctionsUpdates: jest.fn(),
|
||||
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
|
||||
listOfAdditionalFormulaFilters: [],
|
||||
});
|
||||
});
|
||||
@@ -199,4 +212,56 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
|
||||
expect(handleRunQueryMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fx button is disabled when functions already exist', () => {
|
||||
const currentQueryBase = baseQBContext.currentQuery as Query;
|
||||
const supersetQueryBase = baseQBContext.supersetQuery as Query;
|
||||
|
||||
mockedUseQueryBuilder.mockReturnValueOnce({
|
||||
...baseQBContext,
|
||||
currentQuery: {
|
||||
...currentQueryBase,
|
||||
builder: {
|
||||
...currentQueryBase.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQueryBase.builder.queryData[0],
|
||||
functions: [
|
||||
{ name: QueryFunctionsTypes.TIME_SHIFT, args: [] } as QueryFunction,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
supersetQuery: {
|
||||
...supersetQueryBase,
|
||||
builder: {
|
||||
...supersetQueryBase.builder,
|
||||
queryData: [
|
||||
{
|
||||
...supersetQueryBase.builder.queryData[0],
|
||||
functions: [
|
||||
{ name: QueryFunctionsTypes.TIME_SHIFT, args: [] } as QueryFunction,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
|
||||
|
||||
const fxButton = document.querySelector('.function-btn') as HTMLButtonElement;
|
||||
expect(fxButton).toBeInTheDocument();
|
||||
expect(fxButton).toBeDisabled();
|
||||
|
||||
const deleteButton = document.querySelector(
|
||||
'.query-function-delete-btn',
|
||||
) as HTMLButtonElement;
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
userEvent.click(deleteButton);
|
||||
waitFor(() => {
|
||||
expect(fxButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const PREVIOUS_QUERY_KEY = 'previousQuery';
|
||||
|
||||
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
|
||||
try {
|
||||
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
|
||||
} catch {
|
||||
// ignore quota or serialization errors
|
||||
}
|
||||
}
|
||||
|
||||
export const getQueryKey = ({
|
||||
queryName,
|
||||
dataSource,
|
||||
signalSource,
|
||||
panelType,
|
||||
}: {
|
||||
queryName: string;
|
||||
dataSource: string;
|
||||
signalSource: string;
|
||||
panelType: string;
|
||||
}): string => {
|
||||
const qn = queryName || '';
|
||||
const ds = dataSource || '';
|
||||
const ss = signalSource === 'meter' ? 'meter' : '';
|
||||
const pt = panelType || '';
|
||||
return `${qn}:${ds}:${ss}:${pt}`;
|
||||
};
|
||||
|
||||
export const getPreviousQueryFromKey = (key: string): IBuilderQuery | null => {
|
||||
const previousQuery = getPreviousQueryFromStore();
|
||||
return previousQuery?.[key] ?? null;
|
||||
};
|
||||
|
||||
export const saveAsPreviousQuery = (
|
||||
key: string,
|
||||
query: IBuilderQuery,
|
||||
): void => {
|
||||
const previousQuery = getPreviousQueryFromStore();
|
||||
previousQuery[key] = query;
|
||||
writePreviousQueryToStore(previousQuery);
|
||||
};
|
||||
|
||||
export const removeKeyFromPreviousQuery = (key: string): void => {
|
||||
const previousQuery = getPreviousQueryFromStore();
|
||||
delete previousQuery[key];
|
||||
writePreviousQueryToStore(previousQuery);
|
||||
};
|
||||
|
||||
export const clearPreviousQuery = (): void => {
|
||||
try {
|
||||
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,369 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import {
|
||||
Having,
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
import {
|
||||
clearPreviousQuery,
|
||||
PREVIOUS_QUERY_KEY,
|
||||
} from '../QueryV2/previousQuery.utils';
|
||||
|
||||
// Local mocks for domain-specific heavy child components
|
||||
jest.mock(
|
||||
'../QueryV2/QueryAggregation/QueryAggregation',
|
||||
() =>
|
||||
function QueryAggregation(): JSX.Element {
|
||||
return <div>QueryAggregation</div>;
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'../QueryV2/MerticsAggregateSection/MetricsAggregateSection',
|
||||
() =>
|
||||
function MetricsAggregateSection(): JSX.Element {
|
||||
return <div>MetricsAggregateSection</div>;
|
||||
},
|
||||
);
|
||||
// Mock networked children to avoid axios during unit tests
|
||||
jest.mock(
|
||||
'../QueryV2/QuerySearch/QuerySearch',
|
||||
() =>
|
||||
function QuerySearch(): JSX.Element {
|
||||
return <div>QuerySearch</div>;
|
||||
},
|
||||
);
|
||||
jest.mock('container/QueryBuilder/filters', () => ({
|
||||
AggregatorFilter: (): JSX.Element => <div />,
|
||||
}));
|
||||
// Mock hooks
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
describe('MetricsSelect - signal source switching (standalone)', () => {
|
||||
let handleSetQueryDataMock: jest.MockedFunction<
|
||||
(index: number, q: IBuilderQuery) => void
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
clearPreviousQuery();
|
||||
handleSetQueryDataMock = (jest.fn() as unknown) as jest.MockedFunction<
|
||||
(index: number, q: IBuilderQuery) => void
|
||||
>;
|
||||
|
||||
const metricsQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateOperator: '',
|
||||
aggregations: [
|
||||
{
|
||||
timeAggregation: '',
|
||||
metricName: 'test_metric',
|
||||
temporality: '',
|
||||
spaceAggregation: '',
|
||||
},
|
||||
],
|
||||
timeAggregation: '',
|
||||
spaceAggregation: '',
|
||||
temporality: '',
|
||||
functions: [],
|
||||
filter: {
|
||||
expression: 'service = "test"',
|
||||
},
|
||||
filters: { items: [], op: 'AND' },
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
having: [] as Having[],
|
||||
limit: 10,
|
||||
stepInterval: null,
|
||||
orderBy: [],
|
||||
legend: 'A',
|
||||
source: '',
|
||||
};
|
||||
|
||||
const currentQueryObj: Query = {
|
||||
id: 'test',
|
||||
unit: undefined,
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [metricsQuery],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
(mockedUseQueryBuilder as any).mockReturnValue({
|
||||
currentQuery: currentQueryObj,
|
||||
stagedQuery: null,
|
||||
lastUsedQuery: null,
|
||||
setLastUsedQuery: jest.fn(),
|
||||
supersetQuery: currentQueryObj,
|
||||
setSupersetQuery: jest.fn(),
|
||||
initialDataSource: null,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
isEnabledQuery: true,
|
||||
handleSetQueryData: handleSetQueryDataMock,
|
||||
handleSetTraceOperatorData: jest.fn(),
|
||||
handleSetFormulaData: jest.fn(),
|
||||
handleSetQueryItemData: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
removeQueryBuilderEntityByIndex: jest.fn(),
|
||||
removeAllQueryBuilderEntities: jest.fn(),
|
||||
removeQueryTypeItemByIndex: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
removeTraceOperator: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
cloneQuery: jest.fn(),
|
||||
addNewQueryItem: jest.fn(),
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
handleOnUnitsChange: jest.fn(),
|
||||
updateAllQueriesOperators: ((q: any) => q) as any,
|
||||
updateQueriesData: ((q: any) => q) as any,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
clearPreviousQuery();
|
||||
});
|
||||
|
||||
it('savePreviousQuery=true: metrics → meter saves previous query in session storage with appropriate key and query', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
panelType={PANEL_TYPES.TABLE}
|
||||
version="v5"
|
||||
signalSourceChangeEnabled
|
||||
savePreviousQuery
|
||||
/>,
|
||||
);
|
||||
const trigger = document.querySelector(
|
||||
'.metrics-container .source-selector .ant-select-selector',
|
||||
) as HTMLElement;
|
||||
|
||||
await user.click(trigger);
|
||||
|
||||
// wait for dropdown and choose Meter
|
||||
const meterOption = await screen.findByText('Meter');
|
||||
await user.click(meterOption);
|
||||
|
||||
expect(handleSetQueryDataMock).toHaveBeenCalled();
|
||||
const [, arg] = handleSetQueryDataMock.mock.calls[0];
|
||||
expect(arg.queryName).toBe('A');
|
||||
expect(arg.dataSource).toBe(DataSource.METRICS);
|
||||
expect(arg.source).toBe('meter');
|
||||
|
||||
// verify previousQuery store has the expected key and filter expression
|
||||
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
expect(storeRaw).not.toBeNull();
|
||||
const store = JSON.parse(storeRaw || '{}') as Record<string, IBuilderQuery>;
|
||||
expect(Object.keys(store)).toContain('A:metrics::table');
|
||||
expect(store['A:metrics::table']?.filter?.expression).toBe(
|
||||
'service = "test"',
|
||||
);
|
||||
});
|
||||
|
||||
it('savePreviousQuery=false: metrics → meter does not write to sessionStorage and applies defaults', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
// render WITHOUT savePreviousQuery enabled
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
panelType={PANEL_TYPES.TABLE}
|
||||
version="v5"
|
||||
signalSourceChangeEnabled
|
||||
/>,
|
||||
);
|
||||
|
||||
// open Source and choose Meter
|
||||
const trigger = document.querySelector(
|
||||
'.metrics-container .source-selector .ant-select-selector',
|
||||
) as HTMLElement;
|
||||
await user.click(trigger);
|
||||
const meterOption = await screen.findByText('Meter');
|
||||
await user.click(meterOption);
|
||||
|
||||
// assert no session storage written
|
||||
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
// either null or empty object string
|
||||
expect(storeRaw === null || storeRaw === '{}').toBe(true);
|
||||
|
||||
// and query updated to defaults (at least source should be 'meter')
|
||||
expect(handleSetQueryDataMock).toHaveBeenCalled();
|
||||
const [, arg] = handleSetQueryDataMock.mock.calls[0];
|
||||
expect(arg.queryName).toBe('A');
|
||||
expect(arg.dataSource).toBe(DataSource.METRICS);
|
||||
expect(arg.source).toBe('meter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataSource change - Logs to Traces', () => {
|
||||
let handleSetQueryDataMock: jest.MockedFunction<
|
||||
(index: number, q: IBuilderQuery) => void
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
clearPreviousQuery();
|
||||
handleSetQueryDataMock = (jest.fn() as unknown) as jest.MockedFunction<
|
||||
(i: number, q: IBuilderQuery) => void
|
||||
>;
|
||||
|
||||
const logsQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: '',
|
||||
aggregations: [],
|
||||
timeAggregation: '',
|
||||
spaceAggregation: '',
|
||||
temporality: '',
|
||||
functions: [],
|
||||
filter: { expression: 'body CONTAINS "error"' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
having: [] as Having[],
|
||||
limit: 100,
|
||||
stepInterval: null,
|
||||
orderBy: [],
|
||||
legend: 'L',
|
||||
source: '',
|
||||
};
|
||||
|
||||
const logsCurrentQuery: Query = {
|
||||
id: 'test-logs',
|
||||
unit: undefined,
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [logsQuery],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
mockedUseQueryBuilder.mockReset();
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: logsCurrentQuery,
|
||||
stagedQuery: null,
|
||||
lastUsedQuery: null,
|
||||
setLastUsedQuery: jest.fn(),
|
||||
supersetQuery: logsCurrentQuery,
|
||||
setSupersetQuery: jest.fn(),
|
||||
initialDataSource: null,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
isEnabledQuery: true,
|
||||
handleSetQueryData: handleSetQueryDataMock,
|
||||
handleSetTraceOperatorData: jest.fn(),
|
||||
handleSetFormulaData: jest.fn(),
|
||||
handleSetQueryItemData: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
removeQueryBuilderEntityByIndex: jest.fn(),
|
||||
removeAllQueryBuilderEntities: jest.fn(),
|
||||
removeQueryTypeItemByIndex: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
removeTraceOperator: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
cloneQuery: jest.fn(),
|
||||
addNewQueryItem: jest.fn(),
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
resetQuery: jest.fn(),
|
||||
handleOnUnitsChange: jest.fn(),
|
||||
updateAllQueriesOperators: ((q: any) => q) as any,
|
||||
updateQueriesData: ((q: any) => q) as any,
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
clearPreviousQuery();
|
||||
});
|
||||
|
||||
it('updates query dataSource to TRACES saves previous query in session storage', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
panelType={PANEL_TYPES.TABLE}
|
||||
version="v5"
|
||||
signalSourceChangeEnabled
|
||||
savePreviousQuery
|
||||
/>,
|
||||
);
|
||||
|
||||
const logsTrigger = document.querySelector(
|
||||
'.query-data-source .ant-select-selector',
|
||||
) as HTMLElement;
|
||||
await user.click(logsTrigger);
|
||||
|
||||
const tracesContent = screen.getByText('Traces', {
|
||||
selector: '.ant-select-item-option-content',
|
||||
});
|
||||
|
||||
// click the option (prefer the option element; fallback to content)
|
||||
await user.click(
|
||||
(tracesContent.closest('[role="option"]') as HTMLElement) || tracesContent,
|
||||
);
|
||||
|
||||
// verify previousQuery store saved the current LOGS snapshot before switch
|
||||
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
expect(storeRaw).not.toBeNull();
|
||||
const store = JSON.parse(storeRaw || '{}') as Record<string, IBuilderQuery>;
|
||||
expect(Object.keys(store)).toContain('A:logs::table');
|
||||
expect(store['A:logs::table']?.filter?.expression).toBe(
|
||||
'body CONTAINS "error"',
|
||||
);
|
||||
});
|
||||
it('updates query dataSource to TRACES does not saves previous query in session storage', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
panelType={PANEL_TYPES.TABLE}
|
||||
version="v5"
|
||||
signalSourceChangeEnabled
|
||||
/>,
|
||||
);
|
||||
|
||||
const logsTrigger = document.querySelector(
|
||||
'.query-data-source .ant-select-selector',
|
||||
) as HTMLElement;
|
||||
await user.click(logsTrigger);
|
||||
|
||||
const tracesContent = screen.getByText('Traces', {
|
||||
selector: '.ant-select-item-option-content',
|
||||
});
|
||||
|
||||
// click the option (prefer the option element; fallback to content)
|
||||
await user.click(
|
||||
(tracesContent.closest('[role="option"]') as HTMLElement) || tracesContent,
|
||||
);
|
||||
|
||||
// Assert that no snapshot was written
|
||||
const storeRaw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
expect(storeRaw === null || storeRaw === '{}').toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import {
|
||||
clearPreviousQuery,
|
||||
getPreviousQueryFromKey,
|
||||
getQueryKey,
|
||||
PREVIOUS_QUERY_KEY,
|
||||
removeKeyFromPreviousQuery,
|
||||
saveAsPreviousQuery,
|
||||
} from '../QueryV2/previousQuery.utils';
|
||||
|
||||
describe('previousQuery.utils', () => {
|
||||
const sampleQuery: IBuilderQuery = {
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics' as any,
|
||||
aggregateOperator: '',
|
||||
aggregations: [],
|
||||
timeAggregation: '',
|
||||
spaceAggregation: '',
|
||||
temporality: '',
|
||||
functions: [],
|
||||
filter: { expression: 'service = "test"' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
having: [],
|
||||
limit: 10,
|
||||
stepInterval: null,
|
||||
orderBy: [],
|
||||
legend: 'A',
|
||||
source: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
try {
|
||||
sessionStorage.clear();
|
||||
} catch {
|
||||
// jsdom environment
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('getQueryKey normalizes non-meter signal to empty string', () => {
|
||||
const k1 = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: 'metrics', // should normalize to ''
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
const k2 = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: '',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
expect(k1).toBe('A:metrics::TABLE');
|
||||
expect(k2).toBe('A:metrics::TABLE');
|
||||
|
||||
const km = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: 'meter',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
expect(km).toBe('A:metrics:meter:TABLE');
|
||||
});
|
||||
|
||||
it('returns null for missing key when store is empty', () => {
|
||||
expect(getPreviousQueryFromKey('missing:key')).toBeNull();
|
||||
});
|
||||
|
||||
it('saveAsPreviousQuery writes and getPreviousQueryFromKey reads the same object', () => {
|
||||
const key = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: '',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
saveAsPreviousQuery(key, sampleQuery);
|
||||
|
||||
const fromStore = getPreviousQueryFromKey(key);
|
||||
expect(fromStore).toEqual(sampleQuery);
|
||||
});
|
||||
|
||||
it('saveAsPreviousQuery merges multiple entries and removeKeyFromPreviousQuery deletes one', () => {
|
||||
const k1 = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: '',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
const k2 = getQueryKey({
|
||||
queryName: 'B',
|
||||
dataSource: 'metrics',
|
||||
signalSource: 'meter',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
|
||||
saveAsPreviousQuery(k1, sampleQuery);
|
||||
saveAsPreviousQuery(k2, {
|
||||
...sampleQuery,
|
||||
queryName: 'B',
|
||||
source: 'meter' as any,
|
||||
});
|
||||
|
||||
expect(getPreviousQueryFromKey(k1)?.queryName).toBe('A');
|
||||
expect(getPreviousQueryFromKey(k2)?.queryName).toBe('B');
|
||||
|
||||
removeKeyFromPreviousQuery(k1);
|
||||
expect(getPreviousQueryFromKey(k1)).toBeNull();
|
||||
expect(getPreviousQueryFromKey(k2)?.queryName).toBe('B');
|
||||
});
|
||||
|
||||
it('clearPreviousQuery removes the store key', () => {
|
||||
const key = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: '',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
saveAsPreviousQuery(key, sampleQuery);
|
||||
|
||||
expect(sessionStorage.getItem(PREVIOUS_QUERY_KEY)).not.toBeNull();
|
||||
clearPreviousQuery();
|
||||
expect(sessionStorage.getItem(PREVIOUS_QUERY_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('handles malformed JSON in store gracefully', () => {
|
||||
sessionStorage.setItem(PREVIOUS_QUERY_KEY, 'not valid json');
|
||||
|
||||
// Should not throw and behave as empty store
|
||||
expect(getPreviousQueryFromKey('any:key')).toBeNull();
|
||||
|
||||
// After a save, it should overwrite the bad value with valid JSON
|
||||
const validKey = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: '',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
expect(() => saveAsPreviousQuery(validKey, sampleQuery)).not.toThrow();
|
||||
const parsed = JSON.parse(sessionStorage.getItem(PREVIOUS_QUERY_KEY) || '{}');
|
||||
expect(parsed[validKey]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('write errors (e.g., quota) are caught and do not throw', () => {
|
||||
const spy = jest
|
||||
.spyOn(window.sessionStorage.__proto__, 'setItem')
|
||||
.mockImplementation(() => {
|
||||
throw new Error('quota exceeded');
|
||||
});
|
||||
|
||||
const key = getQueryKey({
|
||||
queryName: 'A',
|
||||
dataSource: 'metrics',
|
||||
signalSource: '',
|
||||
panelType: 'TABLE',
|
||||
});
|
||||
|
||||
expect(() => saveAsPreviousQuery(key, sampleQuery)).not.toThrow();
|
||||
|
||||
// Since write failed, reading should still behave as empty
|
||||
spy.mockRestore();
|
||||
expect(getPreviousQueryFromKey(key)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -54,4 +54,5 @@ export enum QueryParams {
|
||||
version = 'version',
|
||||
source = 'source',
|
||||
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
|
||||
isTestAlert = 'isTestAlert',
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ function EndPointDetails({
|
||||
return currentFilters; // No change needed, prevents loop
|
||||
}
|
||||
|
||||
// Rebuild filters: Keep non-http.url filters and add/update http.url filter based on prop
|
||||
// Rebuild filters: Keep non-http_url filters and add/update http_url filter based on prop
|
||||
const otherFilters = currentFilters?.items?.filter(
|
||||
(item) => item.key?.key !== httpUrlKey.key,
|
||||
);
|
||||
@@ -125,7 +125,7 @@ function EndPointDetails({
|
||||
(newFilters: IBuilderQuery['filters']): void => {
|
||||
// 1. Update local filters state immediately
|
||||
setFilters(newFilters);
|
||||
// Filter out http.url filter before saving to params
|
||||
// Filter out http_url filter before saving to params
|
||||
const filteredNewFilters = {
|
||||
op: 'AND',
|
||||
items:
|
||||
|
||||
@@ -56,7 +56,7 @@ function TopErrors({
|
||||
{
|
||||
items: endPointName
|
||||
? [
|
||||
// Remove any existing http.url filters from initialFilters to avoid duplicates
|
||||
// Remove any existing http_url filters from initialFilters to avoid duplicates
|
||||
...(initialFilters?.items?.filter(
|
||||
(item) => item.key?.key !== SPAN_ATTRIBUTES.URL_PATH,
|
||||
) || []),
|
||||
|
||||
@@ -126,12 +126,8 @@ describe('DomainMetrics - V5 Query Payload Tests', () => {
|
||||
'count()',
|
||||
);
|
||||
// Verify exact domain filter expression structure
|
||||
expect(queryA.filter.expression).toContain(
|
||||
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
|
||||
);
|
||||
expect(queryA.filter.expression).toContain(
|
||||
'url.full EXISTS OR http.url EXISTS',
|
||||
);
|
||||
expect(queryA.filter.expression).toContain("http_host = '0.0.0.0'");
|
||||
expect(queryA.filter.expression).toContain('http_url EXISTS');
|
||||
|
||||
// Verify Query B - p99 latency
|
||||
const queryB = queryData.find((q: any) => q.queryName === 'B');
|
||||
@@ -142,17 +138,13 @@ describe('DomainMetrics - V5 Query Payload Tests', () => {
|
||||
'p99(duration_nano)',
|
||||
);
|
||||
// Verify exact domain filter expression structure
|
||||
expect(queryB.filter.expression).toContain(
|
||||
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
|
||||
);
|
||||
expect(queryB.filter.expression).toContain("http_host = '0.0.0.0'");
|
||||
|
||||
// Verify Query C - error count (disabled)
|
||||
const queryC = queryData.find((q: any) => q.queryName === 'C');
|
||||
expect(queryC).toBeDefined();
|
||||
expect(queryC.disabled).toBe(true);
|
||||
expect(queryC.filter.expression).toContain(
|
||||
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
|
||||
);
|
||||
expect(queryC.filter.expression).toContain("http_host = '0.0.0.0'");
|
||||
expect(queryC.aggregations?.[0]).toBeDefined();
|
||||
expect((queryC.aggregations?.[0] as TraceAggregation)?.expression).toBe(
|
||||
'count()',
|
||||
@@ -169,9 +161,7 @@ describe('DomainMetrics - V5 Query Payload Tests', () => {
|
||||
'max(timestamp)',
|
||||
);
|
||||
// Verify exact domain filter expression structure
|
||||
expect(queryD.filter.expression).toContain(
|
||||
"(net.peer.name = '0.0.0.0' OR server.address = '0.0.0.0')",
|
||||
);
|
||||
expect(queryD.filter.expression).toContain("http_host = '0.0.0.0'");
|
||||
|
||||
// Verify Formula F1 - error rate calculation
|
||||
const formulas = payload.query.builder.queryFormulas;
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
|
||||
// Verify exact domain filter expression structure
|
||||
if (queryA.filter) {
|
||||
expect(queryA.filter.expression).toContain(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
|
||||
`http_host = 'api.example.com'`,
|
||||
);
|
||||
expect(queryA.filter.expression).toContain("kind_string = 'Client'");
|
||||
}
|
||||
@@ -171,7 +171,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
|
||||
// Verify exact domain filter expression structure
|
||||
if (queryB.filter) {
|
||||
expect(queryB.filter.expression).toContain(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
|
||||
`http_host = 'api.example.com'`,
|
||||
);
|
||||
expect(queryB.filter.expression).toContain("kind_string = 'Client'");
|
||||
}
|
||||
@@ -185,7 +185,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
|
||||
expect(queryC.aggregateOperator).toBe('count');
|
||||
if (queryC.filter) {
|
||||
expect(queryC.filter.expression).toContain(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
|
||||
`http_host = 'api.example.com'`,
|
||||
);
|
||||
expect(queryC.filter.expression).toContain("kind_string = 'Client'");
|
||||
expect(queryC.filter.expression).toContain('has_error = true');
|
||||
@@ -204,7 +204,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
|
||||
// Verify exact domain filter expression structure
|
||||
if (queryD.filter) {
|
||||
expect(queryD.filter.expression).toContain(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
|
||||
`http_host = 'api.example.com'`,
|
||||
);
|
||||
expect(queryD.filter.expression).toContain("kind_string = 'Client'");
|
||||
}
|
||||
@@ -221,7 +221,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
|
||||
}
|
||||
if (queryE.filter) {
|
||||
expect(queryE.filter.expression).toContain(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com')",
|
||||
`http_host = 'api.example.com'`,
|
||||
);
|
||||
expect(queryE.filter.expression).toContain("kind_string = 'Client'");
|
||||
}
|
||||
@@ -291,7 +291,7 @@ describe('EndPointMetrics - V5 Query Payload Tests', () => {
|
||||
expect(query.filter.expression).toContain('staging');
|
||||
// Also verify domain filter is still present
|
||||
expect(query.filter.expression).toContain(
|
||||
"(net.peer.name = 'api.internal.com' OR server.address = 'api.internal.com')",
|
||||
"http_host = 'api.internal.com'",
|
||||
);
|
||||
// Verify client kind filter is present
|
||||
expect(query.filter.expression).toContain("kind_string = 'Client'");
|
||||
|
||||
@@ -166,6 +166,7 @@ function StatusCodeBarCharts({
|
||||
xValue,
|
||||
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
|
||||
);
|
||||
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
|
||||
@@ -12,8 +12,8 @@ export const VIEW_TYPES = {
|
||||
|
||||
// Span attribute keys - these are the source of truth for all attribute keys
|
||||
export const SPAN_ATTRIBUTES = {
|
||||
URL_PATH: 'http.url',
|
||||
URL_PATH: 'http_url',
|
||||
RESPONSE_STATUS_CODE: 'response_status_code',
|
||||
SERVER_NAME: 'net.peer.name',
|
||||
SERVER_NAME: 'http_host',
|
||||
SERVER_PORT: 'net.peer.port',
|
||||
} as const;
|
||||
|
||||
@@ -350,7 +350,7 @@ describe('API Monitoring Utils', () => {
|
||||
data: {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
[URL_PATH_KEY]: '/api/users',
|
||||
'url.full': 'http://example.com/api/users',
|
||||
URL_PATH_KEY: 'http://example.com/api/users',
|
||||
A: 150, // count or other metric
|
||||
},
|
||||
},
|
||||
@@ -358,7 +358,7 @@ describe('API Monitoring Utils', () => {
|
||||
data: {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
[URL_PATH_KEY]: '/api/orders',
|
||||
'url.full': 'http://example.com/api/orders',
|
||||
URL_PATH_KEY: 'http://example.com/api/orders',
|
||||
A: 75,
|
||||
},
|
||||
},
|
||||
@@ -741,15 +741,6 @@ describe('API Monitoring Utils', () => {
|
||||
if (domainFilter) {
|
||||
expect(domainFilter.value).toBe(domainName);
|
||||
}
|
||||
|
||||
// Should have endpoint filter if provided
|
||||
const endpointFilter = queryData.filters?.items?.find(
|
||||
(item) => item.key && item.key.key === SPAN_ATTRIBUTES.URL_PATH,
|
||||
);
|
||||
expect(endpointFilter).toBeDefined();
|
||||
if (endpointFilter) {
|
||||
expect(endpointFilter.value).toBe(endPointName);
|
||||
}
|
||||
});
|
||||
|
||||
it('should include custom filters in the widget configuration', () => {
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* These tests validate the migration from V4 to V5 format for getAllEndpointsWidgetData:
|
||||
* - Filter format change: filters.items[] → filter.expression
|
||||
* - Aggregation format: aggregateAttribute → aggregations[] array
|
||||
* - Domain filter: (net.peer.name OR server.address)
|
||||
* - Domain filter: http_host = '${domainName}'
|
||||
* - Kind filter: kind_string = 'Client'
|
||||
* - Four queries: A (count), B (p99 latency), C (max timestamp), D (error count - disabled)
|
||||
* - GroupBy: Both http.url AND url.full with type 'attribute'
|
||||
* - GroupBy: http_url with type 'attribute'
|
||||
*/
|
||||
import { getAllEndpointsWidgetData } from 'container/ApiMonitoring/utils';
|
||||
import {
|
||||
@@ -92,28 +92,28 @@ describe('AllEndpointsWidget - V5 Migration Validation', () => {
|
||||
|
||||
const [queryA, queryB, queryC, queryD] = widget.query.builder.queryData;
|
||||
|
||||
const baseExpression = `(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}') AND kind_string = 'Client'`;
|
||||
const baseExpression = `http_host = '${mockDomainName}' AND kind_string = 'Client'`;
|
||||
|
||||
// Queries A, B, C have identical base filter
|
||||
expect(queryA.filter?.expression).toBe(
|
||||
`${baseExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
`${baseExpression} AND http_url EXISTS`,
|
||||
);
|
||||
expect(queryB.filter?.expression).toBe(
|
||||
`${baseExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
`${baseExpression} AND http_url EXISTS`,
|
||||
);
|
||||
expect(queryC.filter?.expression).toBe(
|
||||
`${baseExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
`${baseExpression} AND http_url EXISTS`,
|
||||
);
|
||||
|
||||
// Query D has additional has_error filter
|
||||
expect(queryD.filter?.expression).toBe(
|
||||
`${baseExpression} AND has_error = true AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
`${baseExpression} AND has_error = true AND http_url EXISTS`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. GroupBy Structure', () => {
|
||||
it('default groupBy includes both http.url and url.full with type attribute', () => {
|
||||
it('default groupBy includes both http_url and http_host with type attribute', () => {
|
||||
const widget = getAllEndpointsWidgetData(
|
||||
emptyGroupBy,
|
||||
mockDomainName,
|
||||
@@ -124,23 +124,13 @@ describe('AllEndpointsWidget - V5 Migration Validation', () => {
|
||||
|
||||
// All queries should have the same default groupBy
|
||||
queryData.forEach((query) => {
|
||||
expect(query.groupBy).toHaveLength(2);
|
||||
expect(query.groupBy).toHaveLength(1);
|
||||
|
||||
// http.url
|
||||
expect(query.groupBy).toContainEqual({
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'http.url',
|
||||
type: 'attribute',
|
||||
});
|
||||
|
||||
// url.full
|
||||
expect(query.groupBy).toContainEqual({
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'url.full',
|
||||
key: 'http_url',
|
||||
type: 'attribute',
|
||||
});
|
||||
});
|
||||
@@ -170,19 +160,18 @@ describe('AllEndpointsWidget - V5 Migration Validation', () => {
|
||||
|
||||
// All queries should have defaults + custom groupBy
|
||||
queryData.forEach((query) => {
|
||||
expect(query.groupBy).toHaveLength(4); // 2 defaults + 2 custom
|
||||
expect(query.groupBy).toHaveLength(3); // 1 default + 2 custom
|
||||
|
||||
// First two should be defaults (http.url, url.full)
|
||||
expect(query.groupBy[0].key).toBe('http.url');
|
||||
expect(query.groupBy[1].key).toBe('url.full');
|
||||
// First two should be defaults (http_url)
|
||||
expect(query.groupBy[0].key).toBe('http_url');
|
||||
|
||||
// Last two should be custom (matching subset of properties)
|
||||
expect(query.groupBy[2]).toMatchObject({
|
||||
expect(query.groupBy[1]).toMatchObject({
|
||||
dataType: DataTypes.String,
|
||||
key: 'service.name',
|
||||
type: 'resource',
|
||||
});
|
||||
expect(query.groupBy[3]).toMatchObject({
|
||||
expect(query.groupBy[2]).toMatchObject({
|
||||
dataType: DataTypes.String,
|
||||
key: 'deployment.environment',
|
||||
type: 'resource',
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* These tests validate the migration from V4 to V5 format for the third payload
|
||||
* in getEndPointDetailsQueryPayload (endpoint dropdown data):
|
||||
* - Filter format change: filters.items[] → filter.expression
|
||||
* - Domain handling: (net.peer.name OR server.address)
|
||||
* - Domain handling: http_host = '${domainName}'
|
||||
* - Kind filter: kind_string = 'Client'
|
||||
* - Existence check: (http.url EXISTS OR url.full EXISTS)
|
||||
* - Existence check: http_url EXISTS
|
||||
* - Aggregation: count() expression
|
||||
* - GroupBy: Both http.url AND url.full with type 'attribute'
|
||||
* - GroupBy: http_url with type 'attribute'
|
||||
*/
|
||||
import { getEndPointDetailsQueryPayload } from 'container/ApiMonitoring/utils';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -43,18 +43,16 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
|
||||
expect(typeof queryA.filter?.expression).toBe('string');
|
||||
expect(queryA).not.toHaveProperty('filters');
|
||||
|
||||
// Base filter 1: Domain (net.peer.name OR server.address)
|
||||
// Base filter 1: Domain http_host = '${domainName}'
|
||||
expect(queryA.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Base filter 2: Kind
|
||||
expect(queryA.filter?.expression).toContain("kind_string = 'Client'");
|
||||
|
||||
// Base filter 3: Existence check
|
||||
expect(queryA.filter?.expression).toContain(
|
||||
'(http.url EXISTS OR url.full EXISTS)',
|
||||
);
|
||||
expect(queryA.filter?.expression).toContain('http_url EXISTS');
|
||||
|
||||
// V5 Aggregation format: aggregations array (not aggregateAttribute)
|
||||
expect(queryA.aggregations).toBeDefined();
|
||||
@@ -64,15 +62,10 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
|
||||
});
|
||||
expect(queryA).not.toHaveProperty('aggregateAttribute');
|
||||
|
||||
// GroupBy: Both http.url and url.full
|
||||
expect(queryA.groupBy).toHaveLength(2);
|
||||
// GroupBy: http_url
|
||||
expect(queryA.groupBy).toHaveLength(1);
|
||||
expect(queryA.groupBy).toContainEqual({
|
||||
key: 'http.url',
|
||||
dataType: 'string',
|
||||
type: 'attribute',
|
||||
});
|
||||
expect(queryA.groupBy).toContainEqual({
|
||||
key: 'url.full',
|
||||
key: 'http_url',
|
||||
dataType: 'string',
|
||||
type: 'attribute',
|
||||
});
|
||||
@@ -120,19 +113,19 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
|
||||
|
||||
// Exact filter expression with custom filters merged
|
||||
expect(expression).toBe(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com') AND kind_string = 'Client' AND (http.url EXISTS OR url.full EXISTS) service.name = 'user-service' AND deployment.environment = 'production'",
|
||||
"http_host = 'api.example.com' AND kind_string = 'Client' AND http_url EXISTS service.name = 'user-service' AND deployment.environment = 'production'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. HTTP URL Filter Special Handling', () => {
|
||||
it('converts http.url filter to (http.url OR url.full) expression', () => {
|
||||
it('converts http_url filter to http_url expression', () => {
|
||||
const filtersWithHttpUrl: IBuilderQuery['filters'] = {
|
||||
items: [
|
||||
{
|
||||
id: 'http-url-filter',
|
||||
key: {
|
||||
key: 'http.url',
|
||||
key: 'http_url',
|
||||
dataType: 'string' as any,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -164,9 +157,8 @@ describe('EndpointDropdown - V5 Migration Validation', () => {
|
||||
const expression =
|
||||
dropdownQuery.query.builder.queryData[0].filter?.expression;
|
||||
|
||||
// CRITICAL: Exact filter expression with http.url converted to OR logic
|
||||
expect(expression).toBe(
|
||||
"(net.peer.name = 'api.example.com' OR server.address = 'api.example.com') AND kind_string = 'Client' AND (http.url EXISTS OR url.full EXISTS) service.name = 'user-service' AND (http.url = '/api/users' OR url.full = '/api/users')",
|
||||
"http_host = 'api.example.com' AND kind_string = 'Client' AND http_url EXISTS http_url = '/api/users' AND service.name = 'user-service'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
|
||||
expect(queryData).not.toHaveProperty('filters.items');
|
||||
});
|
||||
|
||||
it('uses new domain filter format: (net.peer.name OR server.address)', () => {
|
||||
it('uses new domain filter format: (http_url)', () => {
|
||||
const widget = getRateOverTimeWidgetData(
|
||||
mockDomainName,
|
||||
mockEndpointName,
|
||||
@@ -44,7 +44,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
|
||||
|
||||
// Verify EXACT new filter format with OR operator
|
||||
expect(queryData?.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Endpoint name is used in legend, not filter
|
||||
@@ -90,7 +90,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
|
||||
|
||||
// Verify domain filter is present
|
||||
expect(queryData?.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Verify custom filters are merged into the expression
|
||||
@@ -120,7 +120,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
|
||||
expect(queryData).not.toHaveProperty('filters.items');
|
||||
});
|
||||
|
||||
it('uses new domain filter format: (net.peer.name OR server.address)', () => {
|
||||
it('uses new domain filter format: (http_host)', () => {
|
||||
const widget = getLatencyOverTimeWidgetData(
|
||||
mockDomainName,
|
||||
mockEndpointName,
|
||||
@@ -132,7 +132,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
|
||||
// Verify EXACT new filter format with OR operator
|
||||
expect(queryData.filter).toBeDefined();
|
||||
expect(queryData?.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Endpoint name is used in legend, not filter
|
||||
@@ -166,7 +166,7 @@ describe('MetricOverTime - V5 Migration Validation', () => {
|
||||
|
||||
// Verify domain filter is present
|
||||
expect(queryData?.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}') service.name = 'user-service'`,
|
||||
`http_host = '${mockDomainName}' service.name = 'user-service'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* V5 Changes:
|
||||
* - Filter format change: filters.items[] → filter.expression
|
||||
* - Domain filter: (net.peer.name OR server.address)
|
||||
* - Domain filter: (http_host)
|
||||
* - Kind filter: kind_string = 'Client'
|
||||
* - stepInterval: 60 → null
|
||||
* - Grouped by response_status_code
|
||||
@@ -47,9 +47,9 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
|
||||
expect(typeof queryA.filter?.expression).toBe('string');
|
||||
expect(queryA).not.toHaveProperty('filters.items');
|
||||
|
||||
// Base filter 1: Domain (net.peer.name OR server.address)
|
||||
// Base filter 1: Domain (http_host)
|
||||
expect(queryA.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Base filter 2: Kind
|
||||
@@ -96,9 +96,9 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
|
||||
expect(typeof queryA.filter?.expression).toBe('string');
|
||||
expect(queryA).not.toHaveProperty('filters.items');
|
||||
|
||||
// Base filter 1: Domain (net.peer.name OR server.address)
|
||||
// Base filter 1: Domain (http_host)
|
||||
expect(queryA.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Base filter 2: Kind
|
||||
@@ -177,7 +177,7 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
|
||||
expect(callsExpression).toBe(latencyExpression);
|
||||
|
||||
// Verify base filters
|
||||
expect(callsExpression).toContain('net.peer.name');
|
||||
expect(callsExpression).toContain('http_host');
|
||||
expect(callsExpression).toContain("kind_string = 'Client'");
|
||||
|
||||
// Verify custom filters are merged
|
||||
@@ -189,13 +189,13 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
|
||||
});
|
||||
|
||||
describe('4. HTTP URL Filter Handling', () => {
|
||||
it('converts http.url filter to (http.url OR url.full) expression in both charts', () => {
|
||||
it('converts http_url filter to http_url expression in both charts', () => {
|
||||
const filtersWithHttpUrl: IBuilderQuery['filters'] = {
|
||||
items: [
|
||||
{
|
||||
id: 'http-url-filter',
|
||||
key: {
|
||||
key: 'http.url',
|
||||
key: 'http_url',
|
||||
dataType: 'string' as any,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -221,16 +221,10 @@ describe('StatusCodeBarCharts - V5 Migration Validation', () => {
|
||||
const latencyExpression =
|
||||
latencyChartQuery.query.builder.queryData[0].filter?.expression;
|
||||
|
||||
// CRITICAL: http.url converted to OR logic
|
||||
expect(callsExpression).toContain(
|
||||
"(http.url = '/api/metrics' OR url.full = '/api/metrics')",
|
||||
);
|
||||
expect(latencyExpression).toContain(
|
||||
"(http.url = '/api/metrics' OR url.full = '/api/metrics')",
|
||||
);
|
||||
expect(callsExpression).toContain("http_url = '/api/metrics'");
|
||||
expect(latencyExpression).toContain("http_url = '/api/metrics'");
|
||||
|
||||
// Base filters still present
|
||||
expect(callsExpression).toContain('net.peer.name');
|
||||
expect(callsExpression).toContain('http_host');
|
||||
expect(callsExpression).toContain("kind_string = 'Client'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* These tests validate the migration from V4 to V5 format for the second payload
|
||||
* in getEndPointDetailsQueryPayload (status code table data):
|
||||
* - Filter format change: filters.items[] → filter.expression
|
||||
* - URL handling: Special logic for (http.url OR url.full)
|
||||
* - Domain filter: (net.peer.name OR server.address)
|
||||
* - URL handling: Special logic for http_url
|
||||
* - Domain filter: http_host = '${domainName}'
|
||||
* - Kind filter: kind_string = 'Client'
|
||||
* - Kind filter: response_status_code EXISTS
|
||||
* - Three queries: A (count), B (p99 latency), C (rate)
|
||||
@@ -45,9 +45,9 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
|
||||
expect(typeof queryA.filter?.expression).toBe('string');
|
||||
expect(queryA).not.toHaveProperty('filters.items');
|
||||
|
||||
// Base filter 1: Domain (net.peer.name OR server.address)
|
||||
// Base filter 1: Domain (http_host)
|
||||
expect(queryA.filter?.expression).toContain(
|
||||
`(net.peer.name = '${mockDomainName}' OR server.address = '${mockDomainName}')`,
|
||||
`http_host = '${mockDomainName}'`,
|
||||
);
|
||||
|
||||
// Base filter 2: Kind
|
||||
@@ -149,7 +149,7 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
|
||||
statusCodeQuery.query.builder.queryData[0].filter?.expression;
|
||||
|
||||
// Base filters present
|
||||
expect(expression).toContain('net.peer.name');
|
||||
expect(expression).toContain('http_host');
|
||||
expect(expression).toContain("kind_string = 'Client'");
|
||||
expect(expression).toContain('response_status_code EXISTS');
|
||||
|
||||
@@ -167,13 +167,13 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
|
||||
});
|
||||
|
||||
describe('4. HTTP URL Filter Handling', () => {
|
||||
it('converts http.url filter to (http.url OR url.full) expression', () => {
|
||||
it('converts http_url filter to http_url expression', () => {
|
||||
const filtersWithHttpUrl: IBuilderQuery['filters'] = {
|
||||
items: [
|
||||
{
|
||||
id: 'http-url-filter',
|
||||
key: {
|
||||
key: 'http.url',
|
||||
key: 'http_url',
|
||||
dataType: 'string' as any,
|
||||
type: 'tag',
|
||||
},
|
||||
@@ -205,17 +205,14 @@ describe('StatusCodeTable - V5 Migration Validation', () => {
|
||||
const expression =
|
||||
statusCodeQuery.query.builder.queryData[0].filter?.expression;
|
||||
|
||||
// CRITICAL: http.url converted to OR logic
|
||||
expect(expression).toContain(
|
||||
"(http.url = '/api/users' OR url.full = '/api/users')",
|
||||
);
|
||||
expect(expression).toContain("http_url = '/api/users'");
|
||||
|
||||
// Other filters still present
|
||||
expect(expression).toContain('service.name');
|
||||
expect(expression).toContain('user-service');
|
||||
|
||||
// Base filters present
|
||||
expect(expression).toContain('net.peer.name');
|
||||
expect(expression).toContain('http_host');
|
||||
expect(expression).toContain("kind_string = 'Client'");
|
||||
expect(expression).toContain('response_status_code EXISTS');
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('TopErrors', () => {
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
name: 'http.url',
|
||||
name: 'http_url',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'attribute',
|
||||
},
|
||||
@@ -123,7 +123,7 @@ describe('TopErrors', () => {
|
||||
table: {
|
||||
rows: [
|
||||
{
|
||||
'http.url': '/api/test',
|
||||
http_url: '/api/test',
|
||||
A: 100,
|
||||
},
|
||||
],
|
||||
@@ -205,7 +205,7 @@ describe('TopErrors', () => {
|
||||
expect(navigateMock).toHaveBeenCalledWith({
|
||||
filters: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'http.url' }),
|
||||
key: expect.objectContaining({ key: 'http_url' }),
|
||||
op: '=',
|
||||
value: '/api/test',
|
||||
}),
|
||||
@@ -215,7 +215,7 @@ describe('TopErrors', () => {
|
||||
value: 'true',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'net.peer.name' }),
|
||||
key: expect.objectContaining({ key: 'http_host' }),
|
||||
op: '=',
|
||||
value: 'test-domain',
|
||||
}),
|
||||
@@ -334,7 +334,7 @@ describe('TopErrors', () => {
|
||||
|
||||
// Verify all required filters are present
|
||||
expect(filterExpression).toContain(
|
||||
`kind_string = 'Client' AND (http.url EXISTS OR url.full EXISTS) AND (net.peer.name = 'test-domain' OR server.address = 'test-domain') AND has_error = true`,
|
||||
`kind_string = 'Client' AND http_url EXISTS AND http_host = 'test-domain' AND has_error = true`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,12 +57,12 @@ export const getDisplayValue = (value: unknown): string =>
|
||||
isEmptyFilterValue(value) ? '-' : String(value);
|
||||
|
||||
export const getDomainNameFilterExpression = (domainName: string): string =>
|
||||
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`;
|
||||
`http_host = '${domainName}'`;
|
||||
|
||||
export const clientKindExpression = `kind_string = 'Client'`;
|
||||
|
||||
/**
|
||||
* Converts filters to expression, handling http.url specially by creating (http.url OR url.full) condition
|
||||
* Converts filters to expression
|
||||
* @param filters Filters to convert
|
||||
* @param baseExpression Base expression to combine with filters
|
||||
* @returns Filter expression string
|
||||
@@ -75,34 +75,6 @@ export const convertFiltersWithUrlHandling = (
|
||||
return baseExpression;
|
||||
}
|
||||
|
||||
// Check if filters contain http.url (SPAN_ATTRIBUTES.URL_PATH)
|
||||
const httpUrlFilter = filters.items?.find(
|
||||
(item) => item.key?.key === SPAN_ATTRIBUTES.URL_PATH,
|
||||
);
|
||||
|
||||
// If http.url filter exists, create modified filters with (http.url OR url.full)
|
||||
if (httpUrlFilter && httpUrlFilter.value) {
|
||||
// Remove ALL http.url filters from items (guards against duplicates)
|
||||
const otherFilters = filters.items?.filter(
|
||||
(item) => item.key?.key !== SPAN_ATTRIBUTES.URL_PATH,
|
||||
);
|
||||
|
||||
// Convert to expression first with other filters
|
||||
const {
|
||||
filter: intermediateFilter,
|
||||
} = convertFiltersToExpressionWithExistingQuery(
|
||||
{ ...filters, items: otherFilters || [] },
|
||||
baseExpression,
|
||||
);
|
||||
|
||||
// Add the OR condition for http.url and url.full
|
||||
const urlValue = httpUrlFilter.value;
|
||||
const urlCondition = `(http.url = '${urlValue}' OR url.full = '${urlValue}')`;
|
||||
return intermediateFilter.expression.trim()
|
||||
? `${intermediateFilter.expression} AND ${urlCondition}`
|
||||
: urlCondition;
|
||||
}
|
||||
|
||||
const { filter } = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
baseExpression,
|
||||
@@ -371,7 +343,7 @@ export const formatDataForTable = (
|
||||
});
|
||||
};
|
||||
|
||||
const urlExpression = `(url.full EXISTS OR http.url EXISTS)`;
|
||||
const urlExpression = `http_url EXISTS`;
|
||||
|
||||
export const getDomainMetricsQueryPayload = (
|
||||
domainName: string,
|
||||
@@ -591,13 +563,6 @@ const defaultGroupBy = [
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'attribute',
|
||||
},
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
key: 'url.full',
|
||||
type: 'attribute',
|
||||
},
|
||||
// {
|
||||
// key: SPAN_ATTRIBUTES.SERVER_PORT,
|
||||
// dataType: DataTypes.Float64,
|
||||
@@ -867,8 +832,8 @@ function buildFilterExpression(
|
||||
): string {
|
||||
const baseFilterParts = [
|
||||
`kind_string = 'Client'`,
|
||||
`(http.url EXISTS OR url.full EXISTS)`,
|
||||
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`,
|
||||
`http_url EXISTS`,
|
||||
`http_host = '${domainName}'`,
|
||||
`has_error = true`,
|
||||
];
|
||||
if (showStatusCodeErrors) {
|
||||
@@ -910,12 +875,7 @@ export const getTopErrorsQueryPayload = (
|
||||
filter: { expression: filterExpression },
|
||||
groupBy: [
|
||||
{
|
||||
name: 'http.url',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'attribute',
|
||||
},
|
||||
{
|
||||
name: 'url.full',
|
||||
name: 'http_url',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: 'attribute',
|
||||
},
|
||||
@@ -1262,9 +1222,7 @@ export const formatTopErrorsDataForTable = (
|
||||
|
||||
return {
|
||||
key: v4(),
|
||||
endpointName: getDisplayValue(
|
||||
rowObj[SPAN_ATTRIBUTES.URL_PATH] || rowObj['url.full'],
|
||||
),
|
||||
endpointName: getDisplayValue(rowObj[SPAN_ATTRIBUTES.URL_PATH]),
|
||||
statusCode: getDisplayValue(rowObj[SPAN_ATTRIBUTES.RESPONSE_STATUS_CODE]),
|
||||
statusMessage: getDisplayValue(rowObj.status_message),
|
||||
count: getDisplayValue(rowObj.__result_0),
|
||||
@@ -1281,10 +1239,10 @@ export const getTopErrorsCoRelationQueryFilters = (
|
||||
{
|
||||
id: 'ea16470b',
|
||||
key: {
|
||||
key: 'http.url',
|
||||
key: 'http_url',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
id: 'http.url--string--tag--false',
|
||||
id: 'http_url--string--tag--false',
|
||||
},
|
||||
op: '=',
|
||||
value: endPointName,
|
||||
@@ -1302,7 +1260,7 @@ export const getTopErrorsCoRelationQueryFilters = (
|
||||
{
|
||||
id: 'e8a043b7',
|
||||
key: {
|
||||
key: 'net.peer.name',
|
||||
key: 'http_host',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
},
|
||||
@@ -1781,7 +1739,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
filters || { items: [], op: 'AND' },
|
||||
`${getDomainNameFilterExpression(
|
||||
domainName,
|
||||
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
)} AND ${clientKindExpression} AND http_url EXISTS`,
|
||||
),
|
||||
},
|
||||
expression: 'A',
|
||||
@@ -1797,11 +1755,6 @@ export const getEndPointDetailsQueryPayload = (
|
||||
dataType: DataTypes.String,
|
||||
type: 'attribute',
|
||||
},
|
||||
{
|
||||
key: 'url.full',
|
||||
dataType: DataTypes.String,
|
||||
type: 'attribute',
|
||||
},
|
||||
],
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
@@ -2420,7 +2373,6 @@ export const statusCodeWidgetInfo = [
|
||||
interface EndPointDropDownResponseRow {
|
||||
data: {
|
||||
[SPAN_ATTRIBUTES.URL_PATH]: string;
|
||||
'url.full': string;
|
||||
A: number;
|
||||
};
|
||||
}
|
||||
@@ -2439,8 +2391,8 @@ export const getFormattedEndPointDropDownData = (
|
||||
}
|
||||
return data.map((row) => ({
|
||||
key: v4(),
|
||||
label: row.data[SPAN_ATTRIBUTES.URL_PATH] || row.data['url.full'] || '-',
|
||||
value: row.data[SPAN_ATTRIBUTES.URL_PATH] || row.data['url.full'] || '-',
|
||||
label: row.data[SPAN_ATTRIBUTES.URL_PATH] || '-',
|
||||
value: row.data[SPAN_ATTRIBUTES.URL_PATH] || '-',
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -2798,20 +2750,6 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
op: '=',
|
||||
value: domainName,
|
||||
},
|
||||
...(endPointName
|
||||
? [
|
||||
{
|
||||
id: '8b1be6f0',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: SPAN_ATTRIBUTES.URL_PATH,
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: endPointName,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(filters?.items || []),
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2933,7 +2871,7 @@ export const getAllEndpointsWidgetData = (
|
||||
filters,
|
||||
`${getDomainNameFilterExpression(
|
||||
domainName,
|
||||
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
)} AND ${clientKindExpression} AND http_url EXISTS`,
|
||||
),
|
||||
},
|
||||
functions: [],
|
||||
@@ -2965,7 +2903,7 @@ export const getAllEndpointsWidgetData = (
|
||||
filters,
|
||||
`${getDomainNameFilterExpression(
|
||||
domainName,
|
||||
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
)} AND ${clientKindExpression} AND http_url EXISTS`,
|
||||
),
|
||||
},
|
||||
functions: [],
|
||||
@@ -2997,7 +2935,7 @@ export const getAllEndpointsWidgetData = (
|
||||
filters,
|
||||
`${getDomainNameFilterExpression(
|
||||
domainName,
|
||||
)} AND ${clientKindExpression} AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
)} AND ${clientKindExpression} AND http_url EXISTS`,
|
||||
),
|
||||
},
|
||||
functions: [],
|
||||
@@ -3029,7 +2967,7 @@ export const getAllEndpointsWidgetData = (
|
||||
filters,
|
||||
`${getDomainNameFilterExpression(
|
||||
domainName,
|
||||
)} AND ${clientKindExpression} AND has_error = true AND (http.url EXISTS OR url.full EXISTS)`,
|
||||
)} AND ${clientKindExpression} AND has_error = true AND http_url EXISTS`,
|
||||
),
|
||||
},
|
||||
functions: [],
|
||||
@@ -3067,10 +3005,10 @@ export const getAllEndpointsWidgetData = (
|
||||
// First try to use the url from the column value
|
||||
let urlValue = url;
|
||||
|
||||
// If url is empty/null and we have the record, fallback to url.full
|
||||
// If url is empty/null and we have the record, fallback to http_url
|
||||
if (isEmptyFilterValue(url) && record) {
|
||||
const { 'url.full': urlFull } = record;
|
||||
urlValue = urlFull;
|
||||
const { http_url } = record;
|
||||
urlValue = http_url;
|
||||
}
|
||||
|
||||
if (!urlValue || urlValue === 'n/a') {
|
||||
@@ -3133,7 +3071,7 @@ export const getAllEndpointsWidgetData = (
|
||||
|
||||
widget.customColTitles = {
|
||||
[SPAN_ATTRIBUTES.URL_PATH]: 'Endpoint',
|
||||
'net.peer.port': 'Port',
|
||||
http_port: 'Port',
|
||||
};
|
||||
|
||||
widget.title = (
|
||||
@@ -3158,12 +3096,10 @@ export const getAllEndpointsWidgetData = (
|
||||
</div>
|
||||
);
|
||||
|
||||
widget.hiddenColumns = ['url.full'];
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
const keysToRemove = ['http.url', 'url.full', 'A', 'B', 'C', 'F1'];
|
||||
const keysToRemove = ['http_url', 'A', 'B', 'C', 'F1'];
|
||||
|
||||
export const getGroupByFiltersFromGroupByValues = (
|
||||
rowData: any,
|
||||
@@ -3221,7 +3157,7 @@ export const getRateOverTimeWidgetData = (
|
||||
filter: {
|
||||
expression: convertFiltersWithUrlHandling(
|
||||
filters || { items: [], op: 'AND' },
|
||||
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`,
|
||||
`http_host = '${domainName}'`,
|
||||
),
|
||||
},
|
||||
functions: [],
|
||||
@@ -3272,7 +3208,7 @@ export const getLatencyOverTimeWidgetData = (
|
||||
filter: {
|
||||
expression: convertFiltersWithUrlHandling(
|
||||
filters || { items: [], op: 'AND' },
|
||||
`(net.peer.name = '${domainName}' OR server.address = '${domainName}')`,
|
||||
`http_host = '${domainName}'`,
|
||||
),
|
||||
},
|
||||
functions: [],
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.app-content {
|
||||
width: calc(100% - 64px); // width of the sidebar
|
||||
width: calc(100% - 54px); // width of the sidebar
|
||||
z-index: 0;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -338,7 +338,7 @@ describe('CreateAlertV2 utils', () => {
|
||||
const props = getCreateAlertLocalStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
basicAlertState: {
|
||||
basic: {
|
||||
...INITIAL_ALERT_STATE,
|
||||
name: 'test-alert',
|
||||
labels: {
|
||||
@@ -348,10 +348,10 @@ describe('CreateAlertV2 utils', () => {
|
||||
yAxisUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
// as we have already verified these utils in their respective tests
|
||||
thresholdState: expect.any(Object),
|
||||
advancedOptionsState: expect.any(Object),
|
||||
evaluationWindowState: expect.any(Object),
|
||||
notificationSettingsState: expect.any(Object),
|
||||
threshold: expect.any(Object),
|
||||
advancedOptions: expect.any(Object),
|
||||
evaluationWindow: expect.any(Object),
|
||||
notificationSettings: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,3 +196,11 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
|
||||
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
|
||||
routingPolicies: false,
|
||||
};
|
||||
|
||||
export const INITIAL_CREATE_ALERT_STATE = {
|
||||
basic: INITIAL_ALERT_STATE,
|
||||
threshold: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
};
|
||||
|
||||
@@ -17,26 +17,22 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { INITIAL_CREATE_ALERT_STATE } from './constants';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from './constants';
|
||||
import {
|
||||
AdvancedOptionsAction,
|
||||
AlertThresholdAction,
|
||||
AlertThresholdMatchType,
|
||||
CreateAlertAction,
|
||||
CreateAlertSlice,
|
||||
EvaluationWindowAction,
|
||||
ICreateAlertContextProps,
|
||||
ICreateAlertProviderProps,
|
||||
NotificationSettingsAction,
|
||||
} from './types';
|
||||
import {
|
||||
advancedOptionsReducer,
|
||||
alertCreationReducer,
|
||||
alertThresholdReducer,
|
||||
buildInitialAlertDef,
|
||||
evaluationWindowReducer,
|
||||
createAlertReducer,
|
||||
getInitialAlertTypeFromURL,
|
||||
notificationSettingsReducer,
|
||||
} from './utils';
|
||||
|
||||
const CreateAlertContext = createContext<ICreateAlertContextProps | null>(null);
|
||||
@@ -65,10 +61,65 @@ export function CreateAlertProvider(
|
||||
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const [alertState, setAlertState] = useReducer(alertCreationReducer, {
|
||||
...INITIAL_ALERT_STATE,
|
||||
yAxisUnit: currentQuery.unit,
|
||||
});
|
||||
const [createAlertState, setCreateAlertState] = useReducer(
|
||||
createAlertReducer,
|
||||
{
|
||||
...INITIAL_CREATE_ALERT_STATE,
|
||||
basic: {
|
||||
...INITIAL_CREATE_ALERT_STATE.basic,
|
||||
yAxisUnit: currentQuery.unit,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const setAlertState = useCallback(
|
||||
(action: CreateAlertAction) => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.BASIC,
|
||||
action,
|
||||
});
|
||||
},
|
||||
[setCreateAlertState],
|
||||
);
|
||||
|
||||
const setThresholdState = useCallback(
|
||||
(action: AlertThresholdAction) => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action,
|
||||
});
|
||||
},
|
||||
[setCreateAlertState],
|
||||
);
|
||||
|
||||
const setEvaluationWindow = useCallback(
|
||||
(action: EvaluationWindowAction) => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.EVALUATION_WINDOW,
|
||||
action,
|
||||
});
|
||||
},
|
||||
[setCreateAlertState],
|
||||
);
|
||||
|
||||
const setAdvancedOptions = useCallback(
|
||||
(action: AdvancedOptionsAction) => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.ADVANCED_OPTIONS,
|
||||
action,
|
||||
});
|
||||
},
|
||||
[setCreateAlertState],
|
||||
);
|
||||
const setNotificationSettings = useCallback(
|
||||
(action: NotificationSettingsAction) => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.NOTIFICATION_SETTINGS,
|
||||
action,
|
||||
});
|
||||
},
|
||||
[setCreateAlertState],
|
||||
);
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
@@ -104,92 +155,56 @@ export function CreateAlertProvider(
|
||||
[redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const [thresholdState, setThresholdState] = useReducer(
|
||||
alertThresholdReducer,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
);
|
||||
|
||||
const [evaluationWindow, setEvaluationWindow] = useReducer(
|
||||
evaluationWindowReducer,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
);
|
||||
|
||||
const [advancedOptions, setAdvancedOptions] = useReducer(
|
||||
advancedOptionsReducer,
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
);
|
||||
|
||||
const [notificationSettings, setNotificationSettings] = useReducer(
|
||||
notificationSettingsReducer,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setThresholdState({
|
||||
type: 'RESET',
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'RESET',
|
||||
},
|
||||
});
|
||||
|
||||
if (thresholdsFromURL) {
|
||||
try {
|
||||
const thresholds = JSON.parse(thresholdsFromURL);
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholds,
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholds,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing thresholds from URL:', error);
|
||||
}
|
||||
|
||||
setEvaluationWindow({
|
||||
type: 'SET_INITIAL_STATE_FOR_METER',
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.EVALUATION_WINDOW,
|
||||
action: {
|
||||
type: 'SET_INITIAL_STATE_FOR_METER',
|
||||
},
|
||||
});
|
||||
|
||||
setThresholdState({
|
||||
type: 'SET_MATCH_TYPE',
|
||||
payload: AlertThresholdMatchType.IN_TOTAL,
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'SET_MATCH_TYPE',
|
||||
payload: AlertThresholdMatchType.IN_TOTAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [alertType, thresholdsFromURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && initialAlertState) {
|
||||
setAlertState({
|
||||
setCreateAlertState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.basicAlertState,
|
||||
});
|
||||
setThresholdState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.thresholdState,
|
||||
});
|
||||
setEvaluationWindow({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.evaluationWindowState,
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.advancedOptionsState,
|
||||
});
|
||||
setNotificationSettings({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.notificationSettingsState,
|
||||
payload: initialAlertState,
|
||||
});
|
||||
}
|
||||
}, [initialAlertState, isEditMode]);
|
||||
|
||||
const discardAlertRule = useCallback(() => {
|
||||
setAlertState({
|
||||
type: 'RESET',
|
||||
});
|
||||
setThresholdState({
|
||||
type: 'RESET',
|
||||
});
|
||||
setEvaluationWindow({
|
||||
type: 'RESET',
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'RESET',
|
||||
});
|
||||
setNotificationSettings({
|
||||
setCreateAlertState({
|
||||
type: 'RESET',
|
||||
});
|
||||
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
|
||||
@@ -212,17 +227,17 @@ export function CreateAlertProvider(
|
||||
|
||||
const contextValue: ICreateAlertContextProps = useMemo(
|
||||
() => ({
|
||||
alertState,
|
||||
alertState: createAlertState.basic,
|
||||
setAlertState,
|
||||
alertType,
|
||||
setAlertType: handleAlertTypeChange,
|
||||
thresholdState,
|
||||
thresholdState: createAlertState.threshold,
|
||||
setThresholdState,
|
||||
evaluationWindow,
|
||||
evaluationWindow: createAlertState.evaluationWindow,
|
||||
setEvaluationWindow,
|
||||
advancedOptions,
|
||||
advancedOptions: createAlertState.advancedOptions,
|
||||
setAdvancedOptions,
|
||||
notificationSettings,
|
||||
notificationSettings: createAlertState.notificationSettings,
|
||||
setNotificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
@@ -234,13 +249,14 @@ export function CreateAlertProvider(
|
||||
isEditMode: isEditMode || false,
|
||||
}),
|
||||
[
|
||||
alertState,
|
||||
createAlertState,
|
||||
setAlertState,
|
||||
setThresholdState,
|
||||
setEvaluationWindow,
|
||||
setAdvancedOptions,
|
||||
setNotificationSettings,
|
||||
alertType,
|
||||
handleAlertTypeChange,
|
||||
thresholdState,
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
notificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
isCreatingAlertRule,
|
||||
|
||||
@@ -9,8 +9,6 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { GetCreateAlertLocalStateFromAlertDefReturn } from '../types';
|
||||
|
||||
export interface ICreateAlertContextProps {
|
||||
alertState: AlertState;
|
||||
setAlertState: Dispatch<CreateAlertAction>;
|
||||
@@ -52,7 +50,7 @@ export interface ICreateAlertContextProps {
|
||||
export interface ICreateAlertProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialAlertType: AlertTypes;
|
||||
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
|
||||
initialAlertState?: CreateAlertState;
|
||||
isEditMode?: boolean;
|
||||
ruleId?: string;
|
||||
}
|
||||
@@ -272,3 +270,31 @@ export type NotificationSettingsAction =
|
||||
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export type CreateAlertState = {
|
||||
basic: AlertState;
|
||||
threshold: AlertThresholdState;
|
||||
advancedOptions: AdvancedOptionsState;
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
notificationSettings: NotificationSettingsState;
|
||||
};
|
||||
|
||||
export enum CreateAlertSlice {
|
||||
BASIC = 'basic',
|
||||
THRESHOLD = 'threshold',
|
||||
ADVANCED_OPTIONS = 'advancedOptions',
|
||||
EVALUATION_WINDOW = 'evaluationWindow',
|
||||
NOTIFICATION_SETTINGS = 'notificationSettings',
|
||||
}
|
||||
|
||||
export type CreateAlertReducerAction =
|
||||
| { slice: CreateAlertSlice.BASIC; action: CreateAlertAction }
|
||||
| { slice: CreateAlertSlice.THRESHOLD; action: AlertThresholdAction }
|
||||
| { slice: CreateAlertSlice.ADVANCED_OPTIONS; action: AdvancedOptionsAction }
|
||||
| { slice: CreateAlertSlice.EVALUATION_WINDOW; action: EvaluationWindowAction }
|
||||
| {
|
||||
slice: CreateAlertSlice.NOTIFICATION_SETTINGS;
|
||||
action: NotificationSettingsAction;
|
||||
}
|
||||
| { type: 'RESET' }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: CreateAlertState };
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_CREATE_ALERT_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from './constants';
|
||||
@@ -28,6 +29,9 @@ import {
|
||||
AlertThresholdAction,
|
||||
AlertThresholdState,
|
||||
CreateAlertAction,
|
||||
CreateAlertReducerAction,
|
||||
CreateAlertSlice,
|
||||
CreateAlertState,
|
||||
EvaluationWindowAction,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsAction,
|
||||
@@ -251,3 +255,57 @@ export const notificationSettingsReducer = (
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const createAlertReducer = (
|
||||
state: CreateAlertState,
|
||||
action: CreateAlertReducerAction,
|
||||
): CreateAlertState => {
|
||||
// Global actions
|
||||
if ('type' in action) {
|
||||
switch (action.type) {
|
||||
case 'RESET':
|
||||
return INITIAL_CREATE_ALERT_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Slice actions
|
||||
switch (action.slice) {
|
||||
case CreateAlertSlice.BASIC:
|
||||
return { ...state, basic: alertCreationReducer(state.basic, action.action) };
|
||||
case CreateAlertSlice.THRESHOLD:
|
||||
return {
|
||||
...state,
|
||||
threshold: alertThresholdReducer(state.threshold, action.action),
|
||||
};
|
||||
case CreateAlertSlice.ADVANCED_OPTIONS:
|
||||
return {
|
||||
...state,
|
||||
advancedOptions: advancedOptionsReducer(
|
||||
state.advancedOptions,
|
||||
action.action,
|
||||
),
|
||||
};
|
||||
case CreateAlertSlice.EVALUATION_WINDOW:
|
||||
return {
|
||||
...state,
|
||||
evaluationWindow: evaluationWindowReducer(
|
||||
state.evaluationWindow,
|
||||
action.action,
|
||||
),
|
||||
};
|
||||
case CreateAlertSlice.NOTIFICATION_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
notificationSettings: notificationSettingsReducer(
|
||||
state.notificationSettings,
|
||||
action.action,
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
AlertThresholdState,
|
||||
CreateAlertState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from './context/types';
|
||||
import { EVALUATION_WINDOW_TIMEFRAME } from './EvaluationSettings/constants';
|
||||
import { GetCreateAlertLocalStateFromAlertDefReturn } from './types';
|
||||
|
||||
export function Spinner(): JSX.Element | null {
|
||||
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
|
||||
@@ -265,14 +265,14 @@ export function getThresholdStateFromAlertDef(
|
||||
|
||||
export function getCreateAlertLocalStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2 | undefined,
|
||||
): GetCreateAlertLocalStateFromAlertDefReturn {
|
||||
): CreateAlertState {
|
||||
if (!alertDef) {
|
||||
return {
|
||||
basicAlertState: INITIAL_ALERT_STATE,
|
||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptionsState: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindowState: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettingsState: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
basic: INITIAL_ALERT_STATE,
|
||||
threshold: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
};
|
||||
}
|
||||
// Basic alert state
|
||||
@@ -294,10 +294,10 @@ export function getCreateAlertLocalStateFromAlertDef(
|
||||
);
|
||||
|
||||
return {
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptionsState,
|
||||
evaluationWindowState,
|
||||
notificationSettingsState,
|
||||
basic: basicAlertState,
|
||||
threshold: thresholdState,
|
||||
advancedOptions: advancedOptionsState,
|
||||
evaluationWindow: evaluationWindowState,
|
||||
notificationSettings: notificationSettingsState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const listViewInitialLogQuery: Query = {
|
||||
},
|
||||
};
|
||||
|
||||
export const listViewInitialTraceQuery = {
|
||||
export const listViewInitialTraceQuery: Query = {
|
||||
// it should be the above commented query
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
@@ -44,7 +45,7 @@ import {
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
@@ -56,7 +57,11 @@ import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import { VariablesSettingsTab } from './types';
|
||||
import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils';
|
||||
import {
|
||||
DEFAULT_ROW_NAME,
|
||||
downloadObjectAsJson,
|
||||
sanitizeDashboardData,
|
||||
} from './utils';
|
||||
|
||||
import './Description.styles.scss';
|
||||
|
||||
@@ -64,28 +69,6 @@ interface DashboardDescriptionProps {
|
||||
handle: FullScreenHandle;
|
||||
}
|
||||
|
||||
export function sanitizeDashboardData(
|
||||
selectedData: DashboardData,
|
||||
): DashboardData {
|
||||
if (!selectedData?.variables) {
|
||||
return selectedData;
|
||||
}
|
||||
|
||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const { selectedValue: _selectedValue, ...rest } = value;
|
||||
acc[key] = rest;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, IDashboardVariable>,
|
||||
);
|
||||
|
||||
return {
|
||||
...selectedData,
|
||||
variables: updatedVariables,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -119,6 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
uuid: selectedDashboard.id,
|
||||
}
|
||||
: ({} as DashboardData);
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { title = '', description, tags, image = Base64Icons[0] } =
|
||||
selectedData || {};
|
||||
@@ -576,7 +560,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
<section className="dashboard-description-section">{description}</section>
|
||||
)}
|
||||
|
||||
{!isEmpty(selectedData.variables) && (
|
||||
{!isEmpty(dashboardVariables) && (
|
||||
<section className="dashboard-variables">
|
||||
<DashboardVariableSelection />
|
||||
</section>
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function sanitizeDashboardData(
|
||||
selectedData: DashboardData,
|
||||
): DashboardData {
|
||||
if (!selectedData?.variables) {
|
||||
return selectedData;
|
||||
}
|
||||
|
||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const { selectedValue: _selectedValue, ...rest } = value;
|
||||
acc[key] = rest;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, IDashboardVariable>,
|
||||
);
|
||||
|
||||
return {
|
||||
...selectedData,
|
||||
variables: updatedVariables,
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadObjectAsJson(
|
||||
exportObj: unknown,
|
||||
exportName: string,
|
||||
|
||||
@@ -14,10 +14,8 @@ import { CustomSelect } from 'components/NewSelect';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
createDynamicVariableToWidgetsMap,
|
||||
getWidgetsHavingDynamicVariableAttribute,
|
||||
} from 'hooks/dashboard/utils';
|
||||
import { useWidgetsByDynamicVariableId } from 'hooks/dashboard/useWidgetsByDynamicVariableId';
|
||||
import { getWidgetsHavingDynamicVariableAttribute } from 'hooks/dashboard/utils';
|
||||
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
@@ -243,23 +241,11 @@ function VariableItem({
|
||||
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
|
||||
|
||||
useEffect(() => {
|
||||
const dynamicVariables = Object.values(
|
||||
selectedDashboard?.data?.variables || {},
|
||||
)?.filter((variable: IDashboardVariable) => variable.type === 'DYNAMIC');
|
||||
|
||||
const widgets =
|
||||
selectedDashboard?.data?.widgets?.filter(
|
||||
(widget) => widget.panelTypes !== PANEL_GROUP_TYPES.ROW,
|
||||
) || [];
|
||||
const widgetsHavingDynamicVariables = createDynamicVariableToWidgetsMap(
|
||||
dynamicVariables,
|
||||
widgets as Widgets[],
|
||||
);
|
||||
|
||||
if (variableData?.id && variableData.id in widgetsHavingDynamicVariables) {
|
||||
setSelectedWidgets(widgetsHavingDynamicVariables[variableData.id] || []);
|
||||
if (variableData?.id && variableData.id in widgetsByDynamicVariableId) {
|
||||
setSelectedWidgets(widgetsByDynamicVariableId[variableData.id] || []);
|
||||
} else if (dynamicVariablesSelectedValue?.name) {
|
||||
const widgets = getWidgetsHavingDynamicVariableAttribute(
|
||||
dynamicVariablesSelectedValue?.name,
|
||||
@@ -275,6 +261,7 @@ function VariableItem({
|
||||
selectedDashboard,
|
||||
variableData.id,
|
||||
variableData.name,
|
||||
widgetsByDynamicVariableId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HolderOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
||||
@@ -17,11 +17,13 @@ import { RowProps } from 'antd/lib';
|
||||
import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types';
|
||||
import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { PenLine, Trash2 } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { TVariableMode } from './types';
|
||||
import VariableItem from './VariableItem/VariableItem';
|
||||
@@ -91,13 +93,10 @@ function VariablesSettings({
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const variables = useMemo(() => selectedDashboard?.data?.variables || {}, [
|
||||
selectedDashboard?.data?.variables,
|
||||
]);
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
|
||||
const [existingVariableNamesMap, setExistingVariableNamesMap] = useState<
|
||||
@@ -147,13 +146,13 @@ function VariablesSettings({
|
||||
const variableNamesMap = {};
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
for (const [key, value] of Object.entries(dashboardVariables)) {
|
||||
const { order, id, name } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...variables[key],
|
||||
...dashboardVariables[key],
|
||||
id,
|
||||
});
|
||||
|
||||
@@ -174,10 +173,10 @@ function VariablesSettings({
|
||||
setVariablesTableData(tableRowData);
|
||||
setVariablesOrderArr(variableOrderArr);
|
||||
setExistingVariableNamesMap(variableNamesMap);
|
||||
}, [variables]);
|
||||
}, [dashboardVariables]);
|
||||
|
||||
const updateVariables = (
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
updatedVariablesData: IDashboardVariables,
|
||||
currentRequestedId?: string,
|
||||
widgetIds?: string[],
|
||||
applyToAll?: boolean,
|
||||
@@ -312,7 +311,7 @@ function VariablesSettings({
|
||||
currentVariableId?: string,
|
||||
): boolean => {
|
||||
// Check if any other dynamic variable already uses this attribute key
|
||||
const isDuplicateAttributeKey = Object.values(variables).some(
|
||||
const isDuplicateAttributeKey = Object.values(dashboardVariables).some(
|
||||
(variable: IDashboardVariable) =>
|
||||
variable.type === 'DYNAMIC' &&
|
||||
variable.dynamicVariablesAttribute === attributeKey &&
|
||||
@@ -422,7 +421,7 @@ function VariablesSettings({
|
||||
{variableViewMode ? (
|
||||
<VariableItem
|
||||
variableData={{ ...variableEditData } as IDashboardVariable}
|
||||
existingVariables={variables}
|
||||
existingVariables={dashboardVariables}
|
||||
onSave={onVariableSaveHandler}
|
||||
onCancel={onDoneVariableViewMode}
|
||||
validateName={validateVariableName}
|
||||
|
||||
@@ -25,12 +25,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.focused {
|
||||
.variable-value {
|
||||
outline: 1px solid var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
display: flex;
|
||||
min-width: 120px;
|
||||
@@ -48,6 +42,11 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
outline: 1px solid var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.variable-select {
|
||||
@@ -99,12 +98,6 @@
|
||||
|
||||
.lightMode {
|
||||
.variable-item {
|
||||
&.focused {
|
||||
.variable-value {
|
||||
border: 1px solid var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
@@ -115,6 +108,11 @@
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
outline: 1px solid var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,3 +122,9 @@
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dashboard-variables-selection-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Row } from 'antd';
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import {
|
||||
useDashboardVariables,
|
||||
useDashboardVariablesSelector,
|
||||
} from 'hooks/dashboard/useDashboardVariables';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -11,20 +14,13 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import DynamicVariableSelection from './DynamicVariableSelection';
|
||||
import {
|
||||
buildDependencies,
|
||||
buildDependencyGraph,
|
||||
buildParentDependencyGraph,
|
||||
IDependencyData,
|
||||
onUpdateVariableNode,
|
||||
} from './util';
|
||||
import { onUpdateVariableNode } from './util';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
const {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
variablesToGetUpdated,
|
||||
@@ -33,14 +29,12 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
|
||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { variables } = data || {};
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
|
||||
const [dependencyData, setDependencyData] = useState<IDependencyData | null>(
|
||||
null,
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const sortedVariablesArray = useDashboardVariablesSelector(
|
||||
(state) => state.sortedVariablesArray,
|
||||
);
|
||||
const dependencyData = useDashboardVariablesSelector(
|
||||
(state) => state.dependencyData,
|
||||
);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
@@ -48,82 +42,44 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (variables) {
|
||||
const tableRowData = [];
|
||||
// Initialize variables with default values if not in URL
|
||||
initializeDefaultVariables(
|
||||
dashboardVariables,
|
||||
getUrlVariables,
|
||||
updateUrlVariable,
|
||||
);
|
||||
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const { id } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...variables[key],
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
tableRowData.sort((a, b) => a.order - b.order);
|
||||
|
||||
setVariablesTableData(tableRowData);
|
||||
|
||||
// Initialize variables with default values if not in URL
|
||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||
}
|
||||
}, [getUrlVariables, updateUrlVariable, variables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (variablesTableData.length > 0) {
|
||||
const depGrp = buildDependencies(variablesTableData);
|
||||
const { order, graph, hasCycle, cycleNodes } = buildDependencyGraph(depGrp);
|
||||
const parentDependencyGraph = buildParentDependencyGraph(graph);
|
||||
|
||||
// cleanup order to only include variables that are of type 'QUERY'
|
||||
const cleanedOrder = order.filter((variable) => {
|
||||
const variableData = variablesTableData.find(
|
||||
(v: IDashboardVariable) => v.name === variable,
|
||||
);
|
||||
return variableData?.type === 'QUERY';
|
||||
});
|
||||
|
||||
setDependencyData({
|
||||
order: cleanedOrder,
|
||||
graph,
|
||||
parentDependencyGraph,
|
||||
hasCycle,
|
||||
cycleNodes,
|
||||
});
|
||||
}
|
||||
}, [variables, variablesTableData]);
|
||||
|
||||
// this handles the case where the dependency order changes i.e. variable list updated via creation or deletion etc. and we need to refetch the variables
|
||||
// also trigger when the global time changes
|
||||
useEffect(
|
||||
() => {
|
||||
if (!isEmpty(dependencyData?.order)) {
|
||||
setVariablesToGetUpdated(dependencyData?.order || []);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[JSON.stringify(dependencyData?.order), minTime, maxTime],
|
||||
// Memoize the order key to avoid unnecessary triggers
|
||||
const dependencyOrderKey = useMemo(
|
||||
() => dependencyData?.order?.join(',') ?? '',
|
||||
[dependencyData?.order],
|
||||
);
|
||||
|
||||
// Trigger refetch when dependency order changes or global time changes
|
||||
useEffect(() => {
|
||||
if (dependencyData?.order && dependencyData.order.length > 0) {
|
||||
setVariablesToGetUpdated(dependencyData?.order || []);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dependencyOrderKey, minTime, maxTime]);
|
||||
|
||||
// Performance optimization: For dynamic variables with allSelected=true, we don't store
|
||||
// individual values in localStorage since we can always derive them from available options.
|
||||
// This makes localStorage much lighter and more efficient.
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
id: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
haveCustomValuesSelected?: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): void => {
|
||||
if (id) {
|
||||
const onValueUpdate = useCallback(
|
||||
(
|
||||
name: string,
|
||||
id: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
haveCustomValuesSelected?: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): void => {
|
||||
// For dynamic variables, only store in localStorage when NOT allSelected
|
||||
// This makes localStorage much lighter by avoiding storing all individual values
|
||||
const variable = variables?.[id] || variables?.[name];
|
||||
const isDynamic = variable?.type === 'DYNAMIC';
|
||||
const variable = dashboardVariables[id] || dashboardVariables[name];
|
||||
const isDynamic = variable.type === 'DYNAMIC';
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
|
||||
|
||||
if (allSelected) {
|
||||
@@ -132,41 +88,39 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
updateUrlVariable(name || id, value);
|
||||
}
|
||||
|
||||
if (selectedDashboard) {
|
||||
setSelectedDashboard((prev) => {
|
||||
if (prev) {
|
||||
const oldVariables = prev?.data.variables;
|
||||
// this is added to handle case where we have two different
|
||||
// schemas for variable response
|
||||
if (oldVariables?.[id]) {
|
||||
oldVariables[id] = {
|
||||
...oldVariables[id],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
haveCustomValuesSelected,
|
||||
};
|
||||
}
|
||||
if (oldVariables?.[name]) {
|
||||
oldVariables[name] = {
|
||||
...oldVariables[name],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
haveCustomValuesSelected,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
data: {
|
||||
...prev?.data,
|
||||
variables: {
|
||||
...oldVariables,
|
||||
},
|
||||
},
|
||||
setSelectedDashboard((prev) => {
|
||||
if (prev) {
|
||||
const oldVariables = { ...prev?.data.variables };
|
||||
// this is added to handle case where we have two different
|
||||
// schemas for variable response
|
||||
if (oldVariables?.[id]) {
|
||||
oldVariables[id] = {
|
||||
...oldVariables[id],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
haveCustomValuesSelected,
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
if (oldVariables?.[name]) {
|
||||
oldVariables[name] = {
|
||||
...oldVariables[name],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
haveCustomValuesSelected,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
data: {
|
||||
...prev?.data,
|
||||
variables: {
|
||||
...oldVariables,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
|
||||
if (dependencyData) {
|
||||
const updatedVariables: string[] = [];
|
||||
@@ -182,48 +136,41 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
} else {
|
||||
setVariablesToGetUpdated((prev) => prev.filter((v) => v !== name));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orderBasedSortedVariables = variablesTableData.sort(
|
||||
(a: { order: number }, b: { order: number }) => a.order - b.order,
|
||||
},
|
||||
[
|
||||
dashboardVariables,
|
||||
updateLocalStorageDashboardVariables,
|
||||
dependencyData,
|
||||
updateUrlVariable,
|
||||
setSelectedDashboard,
|
||||
setVariablesToGetUpdated,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Row style={{ display: 'flex', gap: '12px' }}>
|
||||
{orderBasedSortedVariables &&
|
||||
Array.isArray(orderBasedSortedVariables) &&
|
||||
orderBasedSortedVariables.length > 0 &&
|
||||
orderBasedSortedVariables.map((variable) =>
|
||||
variable.type === 'DYNAMIC' ? (
|
||||
<DynamicVariableSelection
|
||||
key={`${variable.name}${variable.id}${variable.order}`}
|
||||
existingVariables={variables}
|
||||
variableData={{
|
||||
name: variable.name,
|
||||
...variable,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate}
|
||||
/>
|
||||
) : (
|
||||
<VariableItem
|
||||
key={`${variable.name}${variable.id}}${variable.order}`}
|
||||
existingVariables={variables}
|
||||
variableData={{
|
||||
name: variable.name,
|
||||
...variable,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate}
|
||||
variablesToGetUpdated={variablesToGetUpdated}
|
||||
setVariablesToGetUpdated={setVariablesToGetUpdated}
|
||||
dependencyData={dependencyData}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<Row className="dashboard-variables-selection-container">
|
||||
{sortedVariablesArray.map((variable) => {
|
||||
const key = `${variable.name}${variable.id}${variable.order}`;
|
||||
|
||||
return variable.type === 'DYNAMIC' ? (
|
||||
<DynamicVariableSelection
|
||||
key={key}
|
||||
existingVariables={dashboardVariables}
|
||||
variableData={variable}
|
||||
onValueUpdate={onValueUpdate}
|
||||
/>
|
||||
) : (
|
||||
<VariableItem
|
||||
key={key}
|
||||
existingVariables={dashboardVariables}
|
||||
variableData={variable}
|
||||
onValueUpdate={onValueUpdate}
|
||||
variablesToGetUpdated={variablesToGetUpdated}
|
||||
setVariablesToGetUpdated={setVariablesToGetUpdated}
|
||||
dependencyData={dependencyData}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { Input, InputRef } from 'antd';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
interface TextboxVariableInputProps {
|
||||
variableData: IDashboardVariable;
|
||||
handleChange: (inputValue: string) => void;
|
||||
}
|
||||
|
||||
function TextboxVariableInput({
|
||||
variableData,
|
||||
handleChange,
|
||||
}: TextboxVariableInputProps): JSX.Element {
|
||||
const textboxInputRef = useRef<InputRef>(null);
|
||||
const [textboxInputValue, setTextboxInputValue] = useState<string>(
|
||||
(variableData.selectedValue?.toString() ||
|
||||
variableData.defaultValue?.toString()) ??
|
||||
'',
|
||||
);
|
||||
|
||||
const handleInputOnChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTextboxInputValue(event.target.value);
|
||||
},
|
||||
[setTextboxInputValue],
|
||||
);
|
||||
|
||||
const handleInputOnBlur = useCallback(
|
||||
(event: React.FocusEvent<HTMLInputElement>): void => {
|
||||
const value = event.target.value.trim();
|
||||
// If empty, reset to default value
|
||||
if (!value && variableData.defaultValue) {
|
||||
setTextboxInputValue(variableData.defaultValue.toString());
|
||||
handleChange(variableData.defaultValue.toString());
|
||||
} else {
|
||||
handleChange(value);
|
||||
}
|
||||
},
|
||||
[handleChange, variableData.defaultValue],
|
||||
);
|
||||
|
||||
const handleInputOnKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (event.key === 'Enter') {
|
||||
textboxInputRef.current?.blur();
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={textboxInputRef}
|
||||
placeholder="Enter value"
|
||||
data-testid={`variable-textbox-${variableData.id}`}
|
||||
bordered={false}
|
||||
value={textboxInputValue}
|
||||
title={textboxInputValue}
|
||||
onChange={handleInputOnChange}
|
||||
onBlur={handleInputOnBlur}
|
||||
onKeyDown={handleInputOnKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(TextboxVariableInput);
|
||||
@@ -4,18 +4,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { Input, InputRef, Popover, Tooltip, Typography } from 'antd';
|
||||
import { Popover, Tooltip, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import { debounce, isArray, isEmpty, isString } from 'lodash-es';
|
||||
import { isArray, isEmpty, isString } from 'lodash-es';
|
||||
import { IDependencyData } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||
@@ -24,7 +25,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { ALL_SELECT_VALUE, variablePropsToPayloadVariables } from '../utils';
|
||||
import { SelectItemStyle } from './styles';
|
||||
import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
|
||||
import TextboxVariableInput from './TextboxVariableInput';
|
||||
import { areArraysEqual, checkAPIInvocation } from './util';
|
||||
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
@@ -71,15 +73,6 @@ function VariableItem({
|
||||
string | string[] | undefined
|
||||
>(undefined);
|
||||
|
||||
// Local state for textbox input to ensure smooth editing experience
|
||||
const [textboxInputValue, setTextboxInputValue] = useState<string>(
|
||||
(variableData.selectedValue?.toString() ||
|
||||
variableData.defaultValue?.toString()) ??
|
||||
'',
|
||||
);
|
||||
const [isTextboxFocused, setIsTextboxFocused] = useState<boolean>(false);
|
||||
const textboxInputRef = useRef<InputRef>(null);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
@@ -303,7 +296,6 @@ function VariableItem({
|
||||
};
|
||||
|
||||
// do not debounce the above function as we do not need debounce in select variables
|
||||
const debouncedHandleChange = debounce(handleChange, 500);
|
||||
|
||||
const { selectedValue } = variableData;
|
||||
const selectedValueStringified = useMemo(
|
||||
@@ -380,7 +372,7 @@ function VariableItem({
|
||||
}, [variableData.type, variableData.customValue]);
|
||||
|
||||
return (
|
||||
<div className={`variable-item${isTextboxFocused ? ' focused' : ''}`}>
|
||||
<div className="variable-item">
|
||||
<Typography.Text className="variable-name" ellipsis>
|
||||
${variableData.name}
|
||||
{variableData.description && (
|
||||
@@ -392,42 +384,9 @@ function VariableItem({
|
||||
|
||||
<div className="variable-value">
|
||||
{variableData.type === 'TEXTBOX' ? (
|
||||
<Input
|
||||
ref={textboxInputRef}
|
||||
placeholder="Enter value"
|
||||
data-testid={`variable-textbox-${variableData.id}`}
|
||||
bordered={false}
|
||||
value={textboxInputValue}
|
||||
title={textboxInputValue}
|
||||
onChange={(e): void => {
|
||||
setTextboxInputValue(e.target.value);
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setIsTextboxFocused(true);
|
||||
}}
|
||||
onBlur={(e): void => {
|
||||
setIsTextboxFocused(false);
|
||||
const value = e.target.value.trim();
|
||||
// If empty, reset to default value
|
||||
if (!value && variableData.defaultValue) {
|
||||
setTextboxInputValue(variableData.defaultValue.toString());
|
||||
debouncedHandleChange(variableData.defaultValue.toString());
|
||||
} else {
|
||||
debouncedHandleChange(value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
const value = textboxInputValue.trim();
|
||||
if (!value && variableData.defaultValue) {
|
||||
setTextboxInputValue(variableData.defaultValue.toString());
|
||||
debouncedHandleChange(variableData.defaultValue.toString());
|
||||
} else {
|
||||
debouncedHandleChange(value);
|
||||
}
|
||||
textboxInputRef.current?.blur();
|
||||
}
|
||||
}}
|
||||
<TextboxVariableInput
|
||||
variableData={variableData}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
optionsData &&
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useCallback } from 'react';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
@@ -27,7 +28,7 @@ interface UseDashboardVariableUpdateReturn {
|
||||
widgetId?: string,
|
||||
) => void;
|
||||
updateVariables: (
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
updatedVariablesData: IDashboardVariables,
|
||||
currentRequestedId?: string,
|
||||
widgetIds?: string[],
|
||||
applyToAll?: boolean,
|
||||
@@ -106,7 +107,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
|
||||
const updateVariables = useCallback(
|
||||
(
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
updatedVariablesData: IDashboardVariables,
|
||||
currentRequestedId?: string,
|
||||
widgetIds?: string[],
|
||||
applyToAll?: boolean,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { OptionData } from 'components/NewSelect/types';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
IDashboardVariables,
|
||||
IDependencyData,
|
||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function areArraysEqual(
|
||||
a: (string | number | boolean)[],
|
||||
@@ -21,7 +25,7 @@ export function areArraysEqual(
|
||||
|
||||
export const convertVariablesToDbFormat = (
|
||||
variblesArr: IDashboardVariable[],
|
||||
): Dashboard['data']['variables'] =>
|
||||
): IDashboardVariables =>
|
||||
variblesArr.reduce((result, obj: IDashboardVariable) => {
|
||||
const { id } = obj;
|
||||
|
||||
@@ -96,14 +100,6 @@ export const buildDependencies = (
|
||||
return graph;
|
||||
};
|
||||
|
||||
export interface IDependencyData {
|
||||
order: string[];
|
||||
graph: VariableGraph;
|
||||
parentDependencyGraph: VariableGraph;
|
||||
hasCycle: boolean;
|
||||
cycleNodes?: string[];
|
||||
}
|
||||
|
||||
export const buildParentDependencyGraph = (
|
||||
graph: VariableGraph,
|
||||
): VariableGraph => {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
|
||||
import Legend from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import Tooltip from 'lib/uPlotV2/components/Tooltip/Tooltip';
|
||||
import {
|
||||
LegendPosition,
|
||||
TooltipRenderArgs,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import UPlotChart from 'lib/uPlotV2/components/UPlotChart';
|
||||
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
|
||||
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { ChartProps } from '../types';
|
||||
|
||||
const TOOLTIP_WIDTH_PADDING = 60;
|
||||
const TOOLTIP_MIN_WIDTH = 200;
|
||||
|
||||
export default function TimeSeries({
|
||||
legendConfig = { position: LegendPosition.BOTTOM },
|
||||
config,
|
||||
data,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
disableTooltip = false,
|
||||
canPinTooltip = false,
|
||||
timezone,
|
||||
yAxisUnit,
|
||||
decimalPrecision,
|
||||
syncMode,
|
||||
syncKey,
|
||||
onDestroy = _noop,
|
||||
children,
|
||||
layoutChildren,
|
||||
'data-testid': testId,
|
||||
}: ChartProps): JSX.Element {
|
||||
const plotInstanceRef = useRef<uPlot | null>(null);
|
||||
|
||||
const legendComponent = useCallback(
|
||||
(averageLegendWidth: number): React.ReactNode => {
|
||||
return (
|
||||
<Legend
|
||||
config={config}
|
||||
position={legendConfig.position}
|
||||
averageLegendWidth={averageLegendWidth}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[config, legendConfig.position],
|
||||
);
|
||||
|
||||
return (
|
||||
<PlotContextProvider>
|
||||
<ChartLayout
|
||||
config={config}
|
||||
containerWidth={containerWidth}
|
||||
containerHeight={containerHeight}
|
||||
legendConfig={legendConfig}
|
||||
legendComponent={legendComponent}
|
||||
layoutChildren={layoutChildren}
|
||||
>
|
||||
{({ chartWidth, chartHeight, averageLegendWidth }): JSX.Element => (
|
||||
<UPlotChart
|
||||
config={config}
|
||||
data={data}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
plotRef={(plot): void => {
|
||||
plotInstanceRef.current = plot;
|
||||
}}
|
||||
onDestroy={(plot: uPlot): void => {
|
||||
plotInstanceRef.current = null;
|
||||
onDestroy(plot);
|
||||
}}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
{!disableTooltip && (
|
||||
<TooltipPlugin
|
||||
config={config}
|
||||
canPinTooltip={canPinTooltip}
|
||||
syncMode={syncMode}
|
||||
maxWidth={Math.max(
|
||||
TOOLTIP_MIN_WIDTH,
|
||||
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
|
||||
)}
|
||||
syncKey={syncKey}
|
||||
render={(props: TooltipRenderArgs): React.ReactNode => (
|
||||
<Tooltip
|
||||
{...props}
|
||||
timezone={timezone}
|
||||
yAxisUnit={yAxisUnit}
|
||||
decimalPrecision={decimalPrecision}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</ChartLayout>
|
||||
</PlotContextProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
import { LegendConfig } from 'lib/uPlotV2/components/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
|
||||
interface BaseChartProps {
|
||||
width: number;
|
||||
height: number;
|
||||
disableTooltip?: boolean;
|
||||
timezone: string;
|
||||
syncMode?: DashboardCursorSync;
|
||||
syncKey?: string;
|
||||
canPinTooltip?: boolean;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
}
|
||||
|
||||
interface TimeSeriesChartProps extends BaseChartProps {
|
||||
config: UPlotConfigBuilder;
|
||||
legendConfig: LegendConfig;
|
||||
data: uPlot.AlignedData;
|
||||
plotRef?: (plot: uPlot | null) => void;
|
||||
onDestroy?: (plot: uPlot) => void;
|
||||
children?: React.ReactNode;
|
||||
layoutChildren?: React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export type ChartProps = TimeSeriesChartProps;
|
||||
@@ -0,0 +1,133 @@
|
||||
import { MAX_LEGEND_WIDTH } from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import { LegendConfig, LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
export interface ChartDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
legendWidth: number;
|
||||
legendHeight: number;
|
||||
averageLegendWidth: number;
|
||||
}
|
||||
|
||||
const AVG_CHAR_WIDTH = 8;
|
||||
const LEGEND_WIDTH_PERCENTILE = 0.85;
|
||||
const DEFAULT_AVG_LABEL_LENGTH = 15;
|
||||
const BASE_LEGEND_WIDTH = 16;
|
||||
const LEGEND_PADDING = 12;
|
||||
const LEGEND_LINE_HEIGHT = 28;
|
||||
|
||||
/**
|
||||
* Calculates the average width of the legend items based on the labels of the series.
|
||||
* @param legends - The labels of the series.
|
||||
* @returns The average width of the legend items.
|
||||
*/
|
||||
export function calculateAverageLegendWidth(legends: string[]): number {
|
||||
if (legends.length === 0) {
|
||||
return DEFAULT_AVG_LABEL_LENGTH * AVG_CHAR_WIDTH;
|
||||
}
|
||||
|
||||
const lengths = legends.map((l) => l.length).sort((a, b) => a - b);
|
||||
|
||||
const index = Math.ceil(LEGEND_WIDTH_PERCENTILE * lengths.length) - 1;
|
||||
const percentileLength = lengths[Math.max(0, index)];
|
||||
|
||||
return BASE_LEGEND_WIDTH + percentileLength * AVG_CHAR_WIDTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute how much space to give to the chart area vs. the legend.
|
||||
*
|
||||
* - For a RIGHT legend, we reserve a vertical column on the right and shrink the chart width.
|
||||
* - For a BOTTOM legend, we reserve up to two rows below the chart and shrink the chart height.
|
||||
*
|
||||
* Implementation details (high level):
|
||||
* - Approximates legend item width from label text length, using a fixed average char width.
|
||||
* - RIGHT legend:
|
||||
* - `legendWidth` is clamped between 150px and min(MAX_LEGEND_WIDTH, 30% of container width).
|
||||
* - Chart width is `containerWidth - legendWidth`.
|
||||
* - BOTTOM legend:
|
||||
* - Computes how many items fit per row, then uses at most 2 rows.
|
||||
* - `legendHeight` is derived from row count, capped by both a fixed pixel max and a % of container height.
|
||||
* - Chart height is `containerHeight - legendHeight`, never below 0.
|
||||
* - `legendsPerSet` is the number of legend items that fit horizontally, based on the same text-width approximation.
|
||||
*
|
||||
* The returned values are the final chart and legend rectangles (width/height),
|
||||
* plus `legendsPerSet` which hints how many legend items to show per row.
|
||||
*/
|
||||
export function calculateChartDimensions({
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
legendConfig,
|
||||
seriesLabels,
|
||||
}: {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
legendConfig: LegendConfig;
|
||||
seriesLabels: string[];
|
||||
}): ChartDimensions {
|
||||
// Guard: no space to lay out chart or legend
|
||||
if (containerWidth <= 0 || containerHeight <= 0) {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
legendWidth: 0,
|
||||
legendHeight: 0,
|
||||
averageLegendWidth: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Approximate width of a single legend item based on label text.
|
||||
const approxLegendItemWidth = calculateAverageLegendWidth(seriesLabels);
|
||||
const legendItemCount = seriesLabels.length;
|
||||
|
||||
if (legendConfig.position === LegendPosition.RIGHT) {
|
||||
const maxRightLegendWidth = Math.min(MAX_LEGEND_WIDTH, containerWidth * 0.3);
|
||||
const rightLegendWidth = Math.min(
|
||||
Math.max(150, approxLegendItemWidth),
|
||||
maxRightLegendWidth,
|
||||
);
|
||||
|
||||
return {
|
||||
width: Math.max(0, containerWidth - rightLegendWidth),
|
||||
height: containerHeight,
|
||||
legendWidth: rightLegendWidth,
|
||||
legendHeight: containerHeight,
|
||||
// Single vertical list on the right.
|
||||
averageLegendWidth: rightLegendWidth,
|
||||
};
|
||||
}
|
||||
|
||||
const legendRowHeight = LEGEND_LINE_HEIGHT + LEGEND_PADDING;
|
||||
|
||||
const legendItemWidth = Math.ceil(
|
||||
Math.min(approxLegendItemWidth, MAX_LEGEND_WIDTH),
|
||||
);
|
||||
const legendItemsPerRow = Math.max(
|
||||
1,
|
||||
Math.floor((containerWidth - LEGEND_PADDING * 2) / legendItemWidth),
|
||||
);
|
||||
|
||||
const legendRowCount = Math.min(
|
||||
2,
|
||||
Math.ceil(legendItemCount / legendItemsPerRow),
|
||||
);
|
||||
|
||||
const idealBottomLegendHeight =
|
||||
legendRowCount > 1
|
||||
? legendRowCount * legendRowHeight - LEGEND_PADDING
|
||||
: legendRowHeight;
|
||||
|
||||
const maxAllowedLegendHeight = Math.min(2 * legendRowHeight, 80);
|
||||
|
||||
const bottomLegendHeight = Math.min(
|
||||
idealBottomLegendHeight,
|
||||
maxAllowedLegendHeight,
|
||||
);
|
||||
|
||||
return {
|
||||
width: containerWidth,
|
||||
height: Math.max(0, containerHeight - bottomLegendHeight),
|
||||
legendWidth: containerWidth,
|
||||
legendHeight: bottomLegendHeight,
|
||||
averageLegendWidth: legendItemWidth,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.chart-manager-container {
|
||||
width: 100%;
|
||||
max-height: calc(40% - 40px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.chart-manager-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.chart-manager-actions-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { getGraphManagerTableColumns } from 'container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset } from 'container/GridCardLayout/GridCard/FullView/types';
|
||||
import { getDefaultTableDataSet } from 'container/GridCardLayout/GridCard/FullView/utils';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
|
||||
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
|
||||
import './ChartManager.styles.scss';
|
||||
|
||||
interface ChartManagerProps {
|
||||
config: UPlotConfigBuilder;
|
||||
alignedData: uPlot.AlignedData;
|
||||
yAxisUnit?: string;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ChartManager provides a tabular view to manage the visibility of
|
||||
* individual series on a uPlot chart.
|
||||
*
|
||||
* It syncs with the legend state coming from the plot context and
|
||||
* allows users to:
|
||||
* - filter series by label
|
||||
* - toggle individual series on/off
|
||||
* - persist the visibility configuration to local storage.
|
||||
*
|
||||
* @param config - `UPlotConfigBuilder` instance used to derive chart options.
|
||||
* @param alignedData - uPlot aligned data used to build the initial table dataset.
|
||||
* @param yAxisUnit - Optional unit label for Y-axis values shown in the table.
|
||||
* @param onCancel - Optional callback invoked when the user cancels the dialog.
|
||||
*/
|
||||
export default function ChartManager({
|
||||
config,
|
||||
alignedData,
|
||||
yAxisUnit,
|
||||
onCancel,
|
||||
}: ChartManagerProps): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const { legendItemsMap } = useLegendsSync({
|
||||
config,
|
||||
subscribeToFocusChange: false,
|
||||
});
|
||||
const {
|
||||
onToggleSeriesOnOff,
|
||||
onToggleSeriesVisibility,
|
||||
syncSeriesVisibilityToLocalStorage,
|
||||
} = usePlotContext();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
|
||||
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
|
||||
);
|
||||
|
||||
const graphVisibilityState = useMemo(
|
||||
() =>
|
||||
Object.entries(legendItemsMap).reduce<boolean[]>((acc, [key, item]) => {
|
||||
acc[Number(key)] = item.show;
|
||||
return acc;
|
||||
}, []),
|
||||
[legendItemsMap],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTableDataSet(
|
||||
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
|
||||
);
|
||||
}, [alignedData, config]);
|
||||
|
||||
const filterHandler = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = event.target.value.toString().toLowerCase();
|
||||
const updatedDataSet = tableDataSet.map((item) => {
|
||||
if (item.label?.toLocaleLowerCase().includes(value)) {
|
||||
return { ...item, show: true };
|
||||
}
|
||||
return { ...item, show: false };
|
||||
});
|
||||
setTableDataSet(updatedDataSet);
|
||||
},
|
||||
[tableDataSet],
|
||||
);
|
||||
|
||||
const dataSource = useMemo(
|
||||
() =>
|
||||
tableDataSet.filter(
|
||||
(item, index) => index !== 0 && item.show, // skipping the first item as it is the x-axis
|
||||
),
|
||||
[tableDataSet],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getGraphManagerTableColumns({
|
||||
tableDataSet,
|
||||
checkBoxOnChangeHandler: (_e, index) => {
|
||||
onToggleSeriesOnOff(index);
|
||||
},
|
||||
graphVisibilityState,
|
||||
labelClickedHandler: onToggleSeriesVisibility,
|
||||
yAxisUnit,
|
||||
isGraphDisabled: isDashboardLocked,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[tableDataSet, graphVisibilityState, yAxisUnit, isDashboardLocked],
|
||||
);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
syncSeriesVisibilityToLocalStorage();
|
||||
notifications.success({
|
||||
message: 'The updated graphs & legends are saved',
|
||||
});
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
|
||||
|
||||
return (
|
||||
<div className="chart-manager-container">
|
||||
<div className="chart-manager-header">
|
||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||
<div className="chart-manager-actions-container">
|
||||
<Button type="default" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chart-manager-table-container">
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
virtual
|
||||
rowKey="index"
|
||||
scroll={{ y: 200 }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useCallback } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getTimeRangeFromStepInterval,
|
||||
isApmMetric,
|
||||
} from 'container/PanelWrapper/utils';
|
||||
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
|
||||
import {
|
||||
PopoverPosition,
|
||||
useCoordinates,
|
||||
} from 'periscope/components/ContextMenu';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseTimeSeriesContextMenuParams {
|
||||
widget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
}
|
||||
|
||||
export const usePanelContextMenu = ({
|
||||
widget,
|
||||
queryResponse,
|
||||
}: UseTimeSeriesContextMenuParams): {
|
||||
coordinates: { x: number; y: number } | null;
|
||||
popoverPosition: PopoverPosition | null;
|
||||
onClose: () => void;
|
||||
menuItemsConfig: {
|
||||
header?: string | React.ReactNode;
|
||||
items?: React.ReactNode;
|
||||
};
|
||||
clickHandlerWithContextMenu: (...args: any[]) => void;
|
||||
} => {
|
||||
const {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
clickedData,
|
||||
onClose,
|
||||
subMenu,
|
||||
onClick,
|
||||
setSubMenu,
|
||||
} = useCoordinates();
|
||||
|
||||
const { menuItemsConfig } = useGraphContextMenu({
|
||||
widgetId: widget.id || '',
|
||||
query: widget.query,
|
||||
graphData: clickedData,
|
||||
onClose,
|
||||
coordinates,
|
||||
subMenu,
|
||||
setSubMenu,
|
||||
contextLinks: widget.contextLinks,
|
||||
panelType: widget.panelTypes,
|
||||
queryRange: queryResponse,
|
||||
});
|
||||
|
||||
const clickHandlerWithContextMenu = useCallback(
|
||||
(...args: any[]) => {
|
||||
const [
|
||||
xValue,
|
||||
_yvalue,
|
||||
_mouseX,
|
||||
_mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
axesData,
|
||||
focusedSeries,
|
||||
] = args;
|
||||
|
||||
const data = getUplotClickData({
|
||||
metric,
|
||||
queryData,
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
focusedSeries,
|
||||
});
|
||||
|
||||
let timeRange;
|
||||
|
||||
if (axesData && queryData?.queryName) {
|
||||
const compositeQuery = (queryResponse?.data?.params as any)?.compositeQuery;
|
||||
|
||||
if (compositeQuery?.queries) {
|
||||
const specificQuery = compositeQuery.queries.find(
|
||||
(query: any) => query.spec?.name === queryData.queryName,
|
||||
);
|
||||
|
||||
const stepInterval = specificQuery?.spec?.stepInterval || 60;
|
||||
|
||||
timeRange = getTimeRangeFromStepInterval(
|
||||
stepInterval,
|
||||
metric?.clickedTimestamp || xValue,
|
||||
specificQuery?.spec?.signal === DataSource.METRICS &&
|
||||
isApmMetric(specificQuery?.spec?.aggregations[0]?.metricName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data?.record?.queryName) {
|
||||
onClick(data.coord, { ...data.record, label: data.label, timeRange });
|
||||
}
|
||||
},
|
||||
[onClick, queryResponse],
|
||||
);
|
||||
|
||||
return {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
onClose,
|
||||
menuItemsConfig,
|
||||
clickHandlerWithContextMenu,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.chart-layout {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
&--legend-right {
|
||||
flex-direction: row;
|
||||
|
||||
.chart-layout__legend-wrapper {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__legend-wrapper {
|
||||
padding-left: 12px;
|
||||
padding-bottom: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { calculateChartDimensions } from 'container/DashboardContainer/visualization/charts/utils';
|
||||
import { LegendConfig, LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
|
||||
import './ChartLayout.styles.scss';
|
||||
|
||||
export interface ChartLayoutProps {
|
||||
legendComponent: (legendPerSet: number) => React.ReactNode;
|
||||
children: (props: {
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
averageLegendWidth: number;
|
||||
}) => React.ReactNode;
|
||||
layoutChildren?: React.ReactNode;
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
legendConfig: LegendConfig;
|
||||
config: UPlotConfigBuilder;
|
||||
}
|
||||
export default function ChartLayout({
|
||||
legendComponent,
|
||||
children,
|
||||
layoutChildren,
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
legendConfig,
|
||||
config,
|
||||
}: ChartLayoutProps): JSX.Element {
|
||||
const chartDimensions = useMemo(
|
||||
() => {
|
||||
const legendItemsMap = config.getLegendItems();
|
||||
const seriesLabels = Object.values(legendItemsMap)
|
||||
.map((item) => item.label)
|
||||
.filter((label): label is string => label !== undefined);
|
||||
return calculateChartDimensions({
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
legendConfig,
|
||||
seriesLabels,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[containerWidth, containerHeight, legendConfig],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="chart-layout__container">
|
||||
<div
|
||||
className={cx('chart-layout', {
|
||||
'chart-layout--legend-right':
|
||||
legendConfig.position === LegendPosition.RIGHT,
|
||||
})}
|
||||
>
|
||||
<div className="chart-layout__content">
|
||||
{children({
|
||||
chartWidth: chartDimensions.width,
|
||||
chartHeight: chartDimensions.height,
|
||||
averageLegendWidth: chartDimensions.averageLegendWidth,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="chart-layout__legend-wrapper"
|
||||
style={{
|
||||
height: chartDimensions.legendHeight,
|
||||
width: chartDimensions.legendWidth,
|
||||
}}
|
||||
>
|
||||
{legendComponent(chartDimensions.averageLegendWidth)}
|
||||
</div>
|
||||
</div>
|
||||
{layoutChildren}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.panel-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import ChartManager from 'container/DashboardContainer/visualization/components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { LineInterpolation } from 'lib/uPlotV2/config/types';
|
||||
import { ContextMenu } from 'periscope/components/ContextMenu';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
|
||||
function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
panelMode,
|
||||
queryResponse,
|
||||
widget,
|
||||
onDragSelect,
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
} = props;
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widget.id) {
|
||||
graphRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
graphRef.current?.focus();
|
||||
setToScrollWidgetId('');
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [queryResponse]);
|
||||
|
||||
const {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
onClose,
|
||||
menuItemsConfig,
|
||||
clickHandlerWithContextMenu,
|
||||
} = usePanelContextMenu({
|
||||
widget,
|
||||
queryResponse,
|
||||
});
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!queryResponse?.data?.payload) {
|
||||
return [];
|
||||
}
|
||||
return prepareChartData(queryResponse?.data?.payload);
|
||||
}, [queryResponse?.data?.payload]);
|
||||
|
||||
const config = useMemo(() => {
|
||||
const tzDate = (timestamp: number): Date =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value);
|
||||
|
||||
return prepareUPlotConfig({
|
||||
widgetId: widget.id || '',
|
||||
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
|
||||
tzDate,
|
||||
minTimeScale: minTimeScale,
|
||||
maxTimeScale: maxTimeScale,
|
||||
isLogScale: widget?.isLogScale ?? false,
|
||||
thresholds: {
|
||||
scaleKey: 'y',
|
||||
thresholds: (widget.thresholds || []).map((threshold) => ({
|
||||
thresholdValue: threshold.thresholdValue ?? 0,
|
||||
thresholdColor: threshold.thresholdColor,
|
||||
thresholdUnit: threshold.thresholdUnit,
|
||||
thresholdLabel: threshold.thresholdLabel,
|
||||
})),
|
||||
yAxisUnit: widget.yAxisUnit,
|
||||
},
|
||||
yAxisUnit: widget.yAxisUnit || '',
|
||||
softMin: widget.softMin === undefined ? null : widget.softMin,
|
||||
softMax: widget.softMax === undefined ? null : widget.softMax,
|
||||
spanGaps: false,
|
||||
colorMapping: widget.customLegendColors ?? {},
|
||||
lineInterpolation: LineInterpolation.Spline,
|
||||
isDarkMode,
|
||||
onClick: clickHandlerWithContextMenu,
|
||||
onDragSelect,
|
||||
currentQuery: widget.query,
|
||||
panelMode,
|
||||
});
|
||||
}, [
|
||||
widget.id,
|
||||
maxTimeScale,
|
||||
minTimeScale,
|
||||
timezone.value,
|
||||
widget.customLegendColors,
|
||||
widget.isLogScale,
|
||||
widget.softMax,
|
||||
widget.softMin,
|
||||
isDarkMode,
|
||||
queryResponse?.data?.payload,
|
||||
widget.query,
|
||||
widget.thresholds,
|
||||
widget.yAxisUnit,
|
||||
panelMode,
|
||||
clickHandlerWithContextMenu,
|
||||
onDragSelect,
|
||||
]);
|
||||
|
||||
const layoutChildren = useMemo(() => {
|
||||
if (!isFullViewMode) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ChartManager
|
||||
config={config}
|
||||
alignedData={chartData}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onCancel={onToggleModelHandler}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
isFullViewMode,
|
||||
config,
|
||||
chartData,
|
||||
widget.yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="panel-container" ref={graphRef}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<TimeSeries
|
||||
config={config}
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
}}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone.value}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
layoutChildren={layoutChildren}
|
||||
>
|
||||
<ContextMenu
|
||||
coordinates={coordinates}
|
||||
popoverPosition={popoverPosition}
|
||||
title={menuItemsConfig.header as string}
|
||||
items={menuItemsConfig.items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</TimeSeries>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeSeriesPanel;
|
||||
@@ -0,0 +1,170 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
fillMissingXAxisTimestamps,
|
||||
getXAxisTimestamps,
|
||||
} from 'container/DashboardContainer/visualization/panels/utils';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import onClickPlugin, {
|
||||
OnClickPluginOpts,
|
||||
} from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import {
|
||||
DistributionType,
|
||||
DrawStyle,
|
||||
LineInterpolation,
|
||||
LineStyle,
|
||||
SelectionPreferencesSource,
|
||||
VisibilityMode,
|
||||
} from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelMode } from '../types';
|
||||
|
||||
export const prepareChartData = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
): uPlot.AlignedData => {
|
||||
const seriesList = apiResponse?.data?.result || [];
|
||||
const timestampArr = getXAxisTimestamps(seriesList);
|
||||
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
|
||||
|
||||
return [timestampArr, ...yAxisValuesArr];
|
||||
};
|
||||
|
||||
export const prepareUPlotConfig = ({
|
||||
widgetId,
|
||||
apiResponse,
|
||||
tzDate,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isLogScale,
|
||||
thresholds,
|
||||
softMin,
|
||||
softMax,
|
||||
spanGaps,
|
||||
colorMapping,
|
||||
lineInterpolation,
|
||||
isDarkMode,
|
||||
currentQuery,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
yAxisUnit,
|
||||
panelMode,
|
||||
}: {
|
||||
widgetId: string;
|
||||
apiResponse: MetricRangePayloadProps;
|
||||
tzDate: uPlot.LocalDateFromUnix;
|
||||
minTimeScale: number | undefined;
|
||||
maxTimeScale: number | undefined;
|
||||
isLogScale: boolean;
|
||||
softMin: number | null;
|
||||
softMax: number | null;
|
||||
spanGaps: boolean;
|
||||
colorMapping: Record<string, string>;
|
||||
lineInterpolation: LineInterpolation;
|
||||
isDarkMode: boolean;
|
||||
thresholds: ThresholdsDrawHookOptions;
|
||||
currentQuery: Query;
|
||||
yAxisUnit: string;
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
panelMode: PanelMode;
|
||||
}): UPlotConfigBuilder => {
|
||||
const builder = new UPlotConfigBuilder({
|
||||
onDragSelect,
|
||||
widgetId,
|
||||
tzDate,
|
||||
shouldSaveSelectionPreference: panelMode === PanelMode.DASHBOARD_VIEW,
|
||||
selectionPreferencesSource: [
|
||||
PanelMode.DASHBOARD_VIEW,
|
||||
PanelMode.STANDALONE_VIEW,
|
||||
].includes(panelMode)
|
||||
? SelectionPreferencesSource.LOCAL_STORAGE
|
||||
: SelectionPreferencesSource.IN_MEMORY,
|
||||
});
|
||||
|
||||
// X scale – time axis
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
time: true,
|
||||
min: minTimeScale,
|
||||
max: maxTimeScale,
|
||||
logBase: isLogScale ? 10 : undefined,
|
||||
distribution: isLogScale
|
||||
? DistributionType.Logarithmic
|
||||
: DistributionType.Linear,
|
||||
});
|
||||
|
||||
// Y scale – value axis, driven primarily by softMin/softMax and data
|
||||
builder.addScale({
|
||||
scaleKey: 'y',
|
||||
time: false,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
softMin: softMin ?? undefined,
|
||||
softMax: softMax ?? undefined,
|
||||
thresholds,
|
||||
logBase: isLogScale ? 10 : undefined,
|
||||
distribution: isLogScale
|
||||
? DistributionType.Logarithmic
|
||||
: DistributionType.Linear,
|
||||
});
|
||||
|
||||
builder.addThresholds(thresholds);
|
||||
|
||||
if (typeof onClick === 'function') {
|
||||
builder.addPlugin(
|
||||
onClickPlugin({
|
||||
onClick,
|
||||
apiResponse,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
show: true,
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
isLogScale: false,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'y',
|
||||
show: true,
|
||||
side: 3,
|
||||
isDarkMode,
|
||||
isLogScale: false,
|
||||
yAxisUnit,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
apiResponse.data?.result?.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
series.legend || '',
|
||||
);
|
||||
|
||||
const label = currentQuery
|
||||
? getLegend(series, currentQuery, baseLabelName)
|
||||
: baseLabelName;
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Line,
|
||||
label: label,
|
||||
colorMapping,
|
||||
spanGaps,
|
||||
lineStyle: LineStyle.Solid,
|
||||
lineInterpolation,
|
||||
showPoints: VisibilityMode.Never,
|
||||
pointSize: 5,
|
||||
isDarkMode,
|
||||
});
|
||||
});
|
||||
return builder;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Represents the visibility state of a single series in a graph
|
||||
*/
|
||||
export interface SeriesVisibilityItem {
|
||||
label: string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the stored visibility state for a widget/graph
|
||||
*/
|
||||
export interface GraphVisibilityState {
|
||||
name: string;
|
||||
dataIndex: SeriesVisibilityItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Context in which a panel is rendered. Used to vary behavior (e.g. persistence,
|
||||
* interactions) per context.
|
||||
*/
|
||||
export enum PanelMode {
|
||||
/** Panel opened in full-screen / standalone view (e.g. from a dashboard widget). */
|
||||
STANDALONE_VIEW = 'STANDALONE_VIEW',
|
||||
/** Panel in the widget builder while editing a dashboard. */
|
||||
DASHBOARD_EDIT = 'DASHBOARD_EDIT',
|
||||
/** Panel rendered as a widget on a dashboard (read-only view). */
|
||||
DASHBOARD_VIEW = 'DASHBOARD_VIEW',
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { normalizePlotValue } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
export function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
||||
const timestamps = new Set<number>();
|
||||
|
||||
seriesList.forEach((series: { values?: [number, string][] }) => {
|
||||
if (series?.values) {
|
||||
series.values.forEach((value) => {
|
||||
timestamps.add(value[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const timestampsArr = Array.from(timestamps);
|
||||
timestampsArr.sort((a, b) => a - b);
|
||||
|
||||
return timestampsArr;
|
||||
}
|
||||
|
||||
export function fillMissingXAxisTimestamps(
|
||||
timestampArr: number[],
|
||||
data: Array<{ values?: [number, string][] }>,
|
||||
): (number | null)[][] {
|
||||
// Ensure we work with a sorted, de‑duplicated list of x-axis timestamps
|
||||
const canonicalTimestamps = Array.from(new Set(timestampArr)).sort(
|
||||
(a, b) => a - b,
|
||||
);
|
||||
|
||||
return data.map(({ values }) =>
|
||||
buildSeriesYValues(canonicalTimestamps, values),
|
||||
);
|
||||
}
|
||||
|
||||
function buildSeriesYValues(
|
||||
timestamps: number[],
|
||||
values?: [number, string][],
|
||||
): (number | null)[] {
|
||||
if (!values?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const valueByTimestamp = new Map<number, number | null>();
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const [timestamp, rawValue] = values[i];
|
||||
valueByTimestamp.set(timestamp, normalizePlotValue(rawValue));
|
||||
}
|
||||
|
||||
return timestamps.map((timestamp) => {
|
||||
const value = valueByTimestamp.get(timestamp);
|
||||
return value !== undefined ? value : null;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import { GraphVisibilityState, SeriesVisibilityItem } from '../types';
|
||||
|
||||
/**
|
||||
* Retrieves the visibility map for a specific widget from localStorage
|
||||
* @param widgetId - The unique identifier of the widget
|
||||
* @returns A Map of series labels to their visibility state, or null if not found
|
||||
*/
|
||||
export function getStoredSeriesVisibility(
|
||||
widgetId: string,
|
||||
): Map<string, boolean> | null {
|
||||
try {
|
||||
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
|
||||
if (!storedData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visibilityStates: GraphVisibilityState[] = JSON.parse(storedData);
|
||||
const widgetState = visibilityStates.find((state) => state.name === widgetId);
|
||||
|
||||
if (!widgetState?.dataIndex?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Map(widgetState.dataIndex.map((item) => [item.label, item.show]));
|
||||
} catch {
|
||||
// Silently handle parsing errors - fall back to default visibility
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSeriesVisibilityToLocalStorage(
|
||||
widgetId: string,
|
||||
seriesVisibility: SeriesVisibilityItem[],
|
||||
): void {
|
||||
try {
|
||||
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
|
||||
let visibilityStates: GraphVisibilityState[];
|
||||
|
||||
if (!storedData) {
|
||||
visibilityStates = [
|
||||
{
|
||||
name: widgetId,
|
||||
dataIndex: seriesVisibility,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
visibilityStates = JSON.parse(storedData);
|
||||
}
|
||||
const widgetState = visibilityStates.find((state) => state.name === widgetId);
|
||||
|
||||
if (!widgetState) {
|
||||
visibilityStates = [
|
||||
...visibilityStates,
|
||||
{
|
||||
name: widgetId,
|
||||
dataIndex: seriesVisibility,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
widgetState.dataIndex = seriesVisibility;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
JSON.stringify(visibilityStates),
|
||||
);
|
||||
} catch {
|
||||
// Silently handle parsing errors - fall back to default visibility
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { MutableRefObject } from 'react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -156,6 +157,7 @@ describe('PanelWrapper with DragSelect', () => {
|
||||
|
||||
render(
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.STANDALONE_VIEW}
|
||||
widget={mockWidget}
|
||||
queryResponse={mockQueryResponse}
|
||||
onDragSelect={mockOnDragSelect}
|
||||
|
||||
@@ -17,6 +17,7 @@ import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
@@ -79,6 +81,7 @@ function FullView({
|
||||
}, [setCurrentGraphRef]);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const { user } = useAppContext();
|
||||
|
||||
const [editWidget] = useComponentPermission(['edit_widget'], user.role);
|
||||
@@ -114,7 +117,7 @@ function FullView({
|
||||
graphType: getGraphType(selectedPanelType),
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: selectedPanelType === PANEL_TYPES.TABLE,
|
||||
originalGraphType: selectedPanelType,
|
||||
@@ -125,7 +128,7 @@ function FullView({
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
@@ -364,6 +367,7 @@ function FullView({
|
||||
/>
|
||||
)}
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.STANDALONE_VIEW}
|
||||
queryResponse={response}
|
||||
widget={widget}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplor
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||
import useGetResolvedText from 'hooks/dashboard/useGetResolvedText';
|
||||
@@ -407,6 +408,7 @@ function WidgetGraphComponent({
|
||||
ref={graphRef}
|
||||
>
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_VIEW}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -53,7 +53,7 @@ function GridCardGraph({
|
||||
customOnRowClick,
|
||||
customTimeRangeWindowForCoRelation,
|
||||
enableDrillDown,
|
||||
widgetsHavingDynamicVariables,
|
||||
widgetsByDynamicVariableId,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
@@ -226,8 +226,8 @@ function GridCardGraph({
|
||||
? Object.entries(variables).reduce((acc, [id, variable]) => {
|
||||
if (
|
||||
variable.type !== 'DYNAMIC' ||
|
||||
(widgetsHavingDynamicVariables?.[variable.id] &&
|
||||
widgetsHavingDynamicVariables?.[variable.id].includes(widget.id))
|
||||
(widgetsByDynamicVariableId?.[variable.id] &&
|
||||
widgetsByDynamicVariableId?.[variable.id].includes(widget.id))
|
||||
) {
|
||||
return { ...acc, [id]: variable.selectedValue };
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import uPlot from 'uplot';
|
||||
@@ -50,7 +51,7 @@ export interface GridCardGraphProps {
|
||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
isQueryEnabled: boolean;
|
||||
variables?: Dashboard['data']['variables'];
|
||||
variables?: IDashboardVariables;
|
||||
version?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customOnDragSelect?: (start: number, end: number) => void;
|
||||
@@ -71,7 +72,7 @@ export interface GridCardGraphProps {
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||
enableDrillDown?: boolean;
|
||||
widgetsHavingDynamicVariables?: Record<string, string[]>;
|
||||
widgetsByDynamicVariableId?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -14,8 +14,9 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { DEFAULT_ROW_NAME } from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
|
||||
import { useWidgetsByDynamicVariableId } from 'hooks/dashboard/useWidgetsByDynamicVariableId';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -34,7 +35,7 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { IDashboardVariable, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
@@ -79,7 +80,9 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { widgets, variables } = data || {};
|
||||
const { widgets } = data || {};
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { user } = useAppContext();
|
||||
|
||||
@@ -99,21 +102,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
Record<string, { widgets: Layout[]; collapsed: boolean }>
|
||||
>({});
|
||||
|
||||
const widgetsHavingDynamicVariables = useMemo(() => {
|
||||
const dynamicVariables = Object.values(
|
||||
selectedDashboard?.data?.variables || {},
|
||||
)?.filter((variable: IDashboardVariable) => variable.type === 'DYNAMIC');
|
||||
|
||||
const widgets =
|
||||
selectedDashboard?.data?.widgets?.filter(
|
||||
(widget) => widget.panelTypes !== PANEL_GROUP_TYPES.ROW,
|
||||
) || [];
|
||||
|
||||
return createDynamicVariableToWidgetsMap(
|
||||
dynamicVariables,
|
||||
widgets as Widgets[],
|
||||
);
|
||||
}, [selectedDashboard]);
|
||||
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPanelMap(panelMap);
|
||||
@@ -178,11 +167,11 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardName: data.title,
|
||||
numberOfPanels: data.widgets?.length,
|
||||
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||
numberOfVariables: Object.keys(dashboardVariables).length || 0,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [data, selectedDashboard?.id]);
|
||||
}, [dashboardVariables, data, selectedDashboard?.id]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) {
|
||||
@@ -622,13 +611,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
<GridCard
|
||||
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
variables={dashboardVariables}
|
||||
// version={selectedDashboard?.data?.version}
|
||||
version={ENTITY_VERSION_V5}
|
||||
onDragSelect={onDragSelect}
|
||||
dataAvailable={checkIfDataExists}
|
||||
enableDrillDown={enableDrillDown}
|
||||
widgetsHavingDynamicVariables={widgetsHavingDynamicVariables}
|
||||
widgetsByDynamicVariableId={widgetsByDynamicVariableId}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
|
||||
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
@@ -36,14 +35,9 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
|
||||
const queryRangeMutation = useMutation(getSubstituteVars);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const dynamicVariables = useMemo(
|
||||
() =>
|
||||
Object.values(selectedDashboard?.data?.variables || {})?.filter(
|
||||
(variable: IDashboardVariable) => variable.type === 'DYNAMIC',
|
||||
),
|
||||
[selectedDashboard],
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
|
||||
const getUpdatedQuery = useCallback(
|
||||
@@ -59,7 +53,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(selectedDashboard?.data?.variables),
|
||||
originalGraphType: widgetConfig.panelTypes,
|
||||
dynamicVariables,
|
||||
dynamicVariables: dashboardDynamicVariables,
|
||||
});
|
||||
|
||||
// Execute query and process results
|
||||
@@ -68,7 +62,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
// Map query data from API response
|
||||
return mapQueryDataFromApi(queryResult.data.compositeQuery);
|
||||
},
|
||||
[dynamicVariables, globalSelectedInterval, queryRangeMutation],
|
||||
[dashboardDynamicVariables, globalSelectedInterval, queryRangeMutation],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 64px);
|
||||
width: calc(100% - 54px);
|
||||
background: rgb(18, 19, 23);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
@@ -442,7 +442,7 @@
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 64px);
|
||||
width: calc(100% - 54px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
height: calc(100% - 64px);
|
||||
height: calc(100% - 54px);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 1rem;
|
||||
|
||||
@@ -39,8 +39,10 @@ import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { sanitizeDashboardData } from 'container/DashboardContainer/DashboardDescription';
|
||||
import { downloadObjectAsJson } from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import {
|
||||
downloadObjectAsJson,
|
||||
sanitizeDashboardData,
|
||||
} from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
color: var(--l1-foreground);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
color: var(--l2-foreground);
|
||||
max-width: 317px;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
@@ -51,8 +51,8 @@
|
||||
|
||||
.login-form-card {
|
||||
width: 100%;
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
background: var(--bg-neutral-dark-950);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
@@ -174,12 +174,12 @@
|
||||
|
||||
&__docs-button {
|
||||
color: var(--bg-ink-400);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-100);
|
||||
border-color: var(--bg-vanilla-400, #d1d5db);
|
||||
border-color: var(--bg-vanilla-400);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@@ -230,8 +230,8 @@
|
||||
|
||||
.login-form-input {
|
||||
height: 32px;
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l3-border);
|
||||
border-radius: 2px;
|
||||
padding: 6px 8px;
|
||||
font-family: Inter, sans-serif;
|
||||
@@ -239,18 +239,18 @@
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
border-color: var(--primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@
|
||||
font-weight: 400 !important;
|
||||
line-height: 1 !important;
|
||||
letter-spacing: -0.065px !important;
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a) !important;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
@@ -296,19 +296,19 @@
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
color: var(--l3-foreground) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--levels-l3-border, #2c303a) !important;
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
@@ -339,26 +339,26 @@
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--semantic-primary-foreground, #eceef2);
|
||||
color: var(--bg-neutral-dark-50);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
background: var(--primary);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
background: var(--primary);
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -370,12 +370,12 @@
|
||||
}
|
||||
|
||||
.login-form-description {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
color: var(--text-neutral-light-200);
|
||||
}
|
||||
|
||||
.login-form-card {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
background: var(--bg-base-white);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
@@ -383,36 +383,36 @@
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
background: var(--bg-vanilla-200);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
color: var(--text-ink-500);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
color: var(--text-neutral-light-200);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
// Select component light mode styling
|
||||
&.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Label = styled.label`
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
color: var(--l1-foreground);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
@@ -47,29 +47,29 @@ export const FormContainer = styled(Form)`
|
||||
& .ant-input,
|
||||
& .ant-input-password,
|
||||
& .ant-select-selector {
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border-color: var(--levels-l3-border, #2c303a) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
background: var(--l3-background) !important;
|
||||
border-color: var(--l3-border) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
.lightMode & {
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
color: var(--l3-foreground) !important;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input:focus,
|
||||
& .ant-input-password:focus,
|
||||
& .ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 64px);
|
||||
width: calc(100% - 54px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.dashboard-navigation {
|
||||
.run-query-dashboard-btn {
|
||||
min-width: 180px;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
border: none !important;
|
||||
margin-left: 0px !important;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -35,8 +36,11 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
|
||||
import './QuerySection.styles.scss';
|
||||
|
||||
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
||||
function QuerySection({
|
||||
selectedGraph,
|
||||
queryRangeKey,
|
||||
isLoadingQueries,
|
||||
}: QueryProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
handleRunQuery: handleRunQueryFromQueryBuilder,
|
||||
@@ -164,6 +168,7 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
queryComponents={queryComponents}
|
||||
signalSourceChangeEnabled
|
||||
savePreviousQuery
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -237,7 +242,13 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
|
||||
<RunQueryBtn
|
||||
className="run-query-dashboard-btn"
|
||||
label="Stage & Run Query"
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
queryRangeKey={queryRangeKey}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
@@ -248,6 +259,8 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
||||
|
||||
interface QueryProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
queryRangeKey?: QueryKey;
|
||||
isLoadingQueries?: boolean;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplor
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
@@ -183,6 +184,7 @@ function WidgetGraph({
|
||||
}}
|
||||
>
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_EDIT}
|
||||
widget={selectedWidget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -24,8 +25,8 @@ function LeftContainer({
|
||||
setSelectedTracesFields,
|
||||
selectedWidget,
|
||||
requestData,
|
||||
setRequestData,
|
||||
isLoadingPanelData,
|
||||
setRequestData,
|
||||
setQueryResponse,
|
||||
enableDrillDown = false,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
@@ -35,15 +36,20 @@ function LeftContainer({
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
|
||||
enabled: !!stagedQuery,
|
||||
queryKey: [
|
||||
const queryRangeKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedInterval,
|
||||
requestData,
|
||||
minTime,
|
||||
maxTime,
|
||||
],
|
||||
[globalSelectedInterval, requestData, minTime, maxTime],
|
||||
);
|
||||
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
|
||||
enabled: !!stagedQuery,
|
||||
queryKey: queryRangeKey,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
// Update parent component with query response for legend colors
|
||||
@@ -64,7 +70,11 @@ function LeftContainer({
|
||||
enableDrillDown={enableDrillDown}
|
||||
/>
|
||||
<QueryContainer className="query-section-left-container">
|
||||
<QuerySection selectedGraph={selectedGraph} />
|
||||
<QuerySection
|
||||
selectedGraph={selectedGraph}
|
||||
queryRangeKey={queryRangeKey}
|
||||
isLoadingQueries={queryResponse.isFetching}
|
||||
/>
|
||||
{selectedGraph === PANEL_TYPES.LIST && (
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={selectedLogFields}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user