mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-08 10:49:56 +00:00
Compare commits
3 Commits
multiple-t
...
temp/local
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01a795012 | ||
|
|
48e5cf91b3 | ||
|
|
dbad604c6b |
@@ -1,15 +1,35 @@
|
||||
services:
|
||||
init-clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.5.6
|
||||
container_name: init-clickhouse
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
version="v0.0.1"
|
||||
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
|
||||
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
|
||||
cd /tmp
|
||||
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
|
||||
tar -xvzf histogram-quantile.tar.gz
|
||||
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
|
||||
chmod +x /var/lib/clickhouse/user_scripts/histogramQuantile
|
||||
volumes:
|
||||
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
|
||||
restart: on-failure
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:25.5.6
|
||||
container_name: clickhouse
|
||||
volumes:
|
||||
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
|
||||
- ${PWD}/fs/etc/clickhouse-server/config.d/custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
- ${PWD}/fs/etc/clickhouse-server/users.d/users.xml:/etc/clickhouse-server/users.d/users.xml
|
||||
- ${PWD}/fs/tmp/var/lib/clickhouse/:/var/lib/clickhouse/
|
||||
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
|
||||
ports:
|
||||
- '127.0.0.1:8123:8123'
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '0.0.0.0:8123:8123'
|
||||
- '0.0.0.0:9000:9000'
|
||||
tty: true
|
||||
healthcheck:
|
||||
test:
|
||||
@@ -23,6 +43,7 @@ services:
|
||||
retries: 3
|
||||
depends_on:
|
||||
- zookeeper
|
||||
- init-clickhouse
|
||||
environment:
|
||||
- CLICKHOUSE_SKIP_USER_SETUP=1
|
||||
zookeeper:
|
||||
|
||||
@@ -44,4 +44,6 @@
|
||||
<shard>01</shard>
|
||||
<replica>01</replica>
|
||||
</macros>
|
||||
<!-- Configuration of user defined executable functions -->
|
||||
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
|
||||
</clickhouse>
|
||||
@@ -0,0 +1,22 @@
|
||||
<functions>
|
||||
<function>
|
||||
<type>executable</type>
|
||||
<name>histogramQuantile</name>
|
||||
<return_type>Float64</return_type>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>buckets</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Array(Float64)</type>
|
||||
<name>counts</name>
|
||||
</argument>
|
||||
<argument>
|
||||
<type>Float64</type>
|
||||
<name>quantile</name>
|
||||
</argument>
|
||||
<format>CSV</format>
|
||||
<command>./histogramQuantile</command>
|
||||
</function>
|
||||
</functions>
|
||||
|
||||
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -132,6 +132,3 @@
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
## UplotV2
|
||||
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
|
||||
5
.github/workflows/integrationci.yaml
vendored
5
.github/workflows/integrationci.yaml
vendored
@@ -42,11 +42,10 @@ jobs:
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
- dashboard
|
||||
- logspipelines
|
||||
- preference
|
||||
- querier
|
||||
- role
|
||||
- ttl
|
||||
- preference
|
||||
- logspipelines
|
||||
- alerts
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -11,8 +11,5 @@
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.1
|
||||
image: signoz/signoz:v0.110.0
|
||||
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.1
|
||||
image: signoz/signoz:v0.110.0
|
||||
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.1}
|
||||
image: signoz/signoz:${VERSION:-v0.110.0}
|
||||
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.1}
|
||||
image: signoz/signoz:${VERSION:-v0.110.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -2285,15 +2285,6 @@ 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:
|
||||
@@ -2606,19 +2597,6 @@ 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:
|
||||
|
||||
@@ -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.Name, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil)
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, 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.Name, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, 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.Name, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, 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, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||
func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, 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(name, orgID, relation, additions)
|
||||
additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
|
||||
deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ type GetAnomaliesResponse struct {
|
||||
//
|
||||
// ^ ^
|
||||
// | |
|
||||
// (rounded value for past period) + (seasonal growth)
|
||||
// (rounded value for past peiod) + (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 periods to the current season
|
||||
// Past2SeasonQuery is the query range params for past 2 seasonal period 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 periods to the current season
|
||||
// Past3SeasonQuery is the query range params for past 3 seasonal period 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,11 +234,6 @@ 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)
|
||||
@@ -290,11 +285,6 @@ 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)
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
|
||||
}
|
||||
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
|
||||
@@ -39,7 +39,7 @@ type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
|
||||
* This endpoints 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 endpoint returns public sharing config for a dashboard
|
||||
* This endpoints returns public sharing config for a dashboard
|
||||
* @summary Get public dashboard
|
||||
*/
|
||||
export const getPublicDashboard = (
|
||||
@@ -222,7 +222,7 @@ export const invalidateGetPublicDashboard = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates public sharing config and enables public sharing of the dashboard
|
||||
* This endpoints 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 endpoint updates the public sharing config for a dashboard
|
||||
* This endpoints 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 endpoint returns the sanitized dashboard data for public access
|
||||
* This endpoints returns the sanitized dashboard data for public access
|
||||
* @summary Get public dashboard data
|
||||
*/
|
||||
export const getPublicDashboardData = (
|
||||
|
||||
@@ -25,7 +25,7 @@ type AwaitedInput<T> = PromiseLike<T> | T;
|
||||
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
|
||||
|
||||
/**
|
||||
* This endpoint returns global config
|
||||
* This endpoints returns global config
|
||||
* @summary Get global config
|
||||
*/
|
||||
export const getGlobalConfig = (signal?: AbortSignal) => {
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
/**
|
||||
* ! 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);
|
||||
};
|
||||
@@ -1110,39 +1110,6 @@ 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
|
||||
@@ -1343,21 +1310,6 @@ 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
|
||||
@@ -1389,11 +1341,6 @@ export interface TypesPostableResetPasswordDTO {
|
||||
}
|
||||
|
||||
export interface TypesResetPasswordTokenDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
expiresAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -1735,42 +1682,6 @@ 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
|
||||
|
||||
@@ -40,7 +40,6 @@ import type {
|
||||
TypesChangePasswordRequestDTO,
|
||||
TypesPostableAcceptInviteDTO,
|
||||
TypesPostableAPIKeyDTO,
|
||||
TypesPostableForgotPasswordDTO,
|
||||
TypesPostableInviteDTO,
|
||||
TypesPostableResetPasswordDTO,
|
||||
TypesStorableAPIKeyDTO,
|
||||
@@ -1568,87 +1567,3 @@ 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);
|
||||
border: 1px solid var(--bg-ink-200);
|
||||
background: var(--bg-ink-400, #121317);
|
||||
border: 1px solid var(--bg-ink-200, #23262e);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: var(--text-neutral-dark-100);
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
.auth-footer-link-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-neutral-dark-50);
|
||||
color: var(--text-neutral-dark-50, #eceef2);
|
||||
}
|
||||
|
||||
.auth-footer-link-status {
|
||||
@@ -84,14 +84,14 @@
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-ink-200);
|
||||
background: var(--bg-ink-200, #23262e);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-footer-content {
|
||||
background: var(--bg-base-white);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@@ -102,14 +102,14 @@
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-neutral-light-200);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.auth-footer-link-icon {
|
||||
color: var(--text-neutral-light-100);
|
||||
color: var(--text-neutral-light-100, #62636c);
|
||||
}
|
||||
|
||||
.auth-footer-separator {
|
||||
background: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-300, #e9e9e9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
font-size: 15.4px;
|
||||
font-weight: 500;
|
||||
line-height: 17.5px;
|
||||
color: var(--text-neutral-dark-50);
|
||||
color: var(--text-neutral-dark-50, #eceef2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-ink-400);
|
||||
background: var(--bg-ink-400, #121317);
|
||||
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);
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-neutral-dark-100);
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -68,15 +68,15 @@
|
||||
|
||||
.lightMode {
|
||||
.auth-header-logo-text {
|
||||
color: var(--text-neutral-light-100);
|
||||
color: var(--text-neutral-light-100, #62636c);
|
||||
}
|
||||
|
||||
.auth-header-help-button {
|
||||
background: var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
|
||||
span,
|
||||
svg {
|
||||
color: var(--text-neutral-light-200);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background: var(--bg-neutral-dark-1000);
|
||||
background: var(--bg-neutral-dark-1000, #0a0c10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
.bg-dot-pattern {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
var(--bg-neutral-dark-50) 1px,
|
||||
var(--bg-neutral-dark-50, #eceef2) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 12px 12px;
|
||||
@@ -85,8 +85,8 @@
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-ink-200) 0px,
|
||||
var(--bg-ink-200) 4px,
|
||||
var(--bg-ink-200, #23262e) 0px,
|
||||
var(--bg-ink-200, #23262e) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
.lightMode {
|
||||
.auth-page-wrapper {
|
||||
background: var(--bg-base-white);
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
}
|
||||
|
||||
.bg-dot-pattern {
|
||||
@@ -172,8 +172,8 @@
|
||||
.auth-page-line-right {
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300) 0px,
|
||||
var(--bg-vanilla-300) 4px,
|
||||
var(--bg-vanilla-300, #e9e9e9) 0px,
|
||||
var(--bg-vanilla-300, #e9e9e9) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ 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';
|
||||
@@ -25,7 +24,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
version,
|
||||
onSignalSourceChange,
|
||||
signalSourceChangeEnabled = false,
|
||||
savePreviousQuery = false,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
@@ -63,14 +61,6 @@ 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],
|
||||
@@ -210,7 +200,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={1}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
) : (
|
||||
currentQuery.builder.queryData.map((query, index) => (
|
||||
@@ -233,7 +222,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={currentQuery.builder.queryData.length}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -13,13 +13,6 @@ 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>[] = [
|
||||
@@ -34,7 +27,6 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
signalSource,
|
||||
onSignalSourceChange,
|
||||
signalSourceChangeEnabled = false,
|
||||
savePreviousQuery = false,
|
||||
}: {
|
||||
query: IBuilderQuery;
|
||||
index: number;
|
||||
@@ -42,7 +34,6 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
signalSource: 'meter' | '';
|
||||
onSignalSourceChange: (value: string) => void;
|
||||
signalSourceChangeEnabled: boolean;
|
||||
savePreviousQuery: boolean;
|
||||
}): JSX.Element {
|
||||
const [attributeKeys, setAttributeKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
|
||||
@@ -59,11 +50,7 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
[handleChangeAggregatorAttribute, attributeKeys],
|
||||
);
|
||||
|
||||
const {
|
||||
updateAllQueriesOperators,
|
||||
handleSetQueryData,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const { updateAllQueriesOperators, handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
const source = useMemo(
|
||||
() => (signalSource === 'meter' ? 'meter' : 'metrics'),
|
||||
@@ -92,63 +79,22 @@ 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, newQueryData);
|
||||
handleSetQueryData(
|
||||
index,
|
||||
value === 'meter'
|
||||
? {
|
||||
...defaultMeterQuery.builder.queryData[0],
|
||||
source: 'meter',
|
||||
queryName: query.queryName,
|
||||
}
|
||||
: {
|
||||
...defaultMetricsQuery.builder.queryData[0],
|
||||
source: '',
|
||||
queryName: query.queryName,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,16 +28,17 @@ 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';
|
||||
@@ -206,9 +207,14 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const dynamicVariables = useMemo(
|
||||
() =>
|
||||
Object.values(selectedDashboard?.data?.variables || {})?.filter(
|
||||
(variable: IDashboardVariable) => variable.type === 'DYNAMIC',
|
||||
),
|
||||
[selectedDashboard],
|
||||
);
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
@@ -1063,7 +1069,7 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
// Add dynamic variables suggestions for the current key
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
const variableName = dynamicVariables?.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
||||
)?.name;
|
||||
|
||||
|
||||
@@ -42,12 +42,10 @@ 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 {
|
||||
@@ -69,7 +67,6 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
filterConfigs,
|
||||
isListViewPanel,
|
||||
entityVersion: version,
|
||||
savePreviousQuery,
|
||||
});
|
||||
|
||||
const handleToggleDisableQuery = useCallback(() => {
|
||||
@@ -237,7 +234,6 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
signalSource={signalSource as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,14 +11,9 @@ 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,
|
||||
QueryFunctionsTypes,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
@@ -50,17 +45,12 @@ 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,
|
||||
@@ -101,7 +91,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
const updateQueriesData: QueryBuilderContextType['updateQueriesData'] = (q) =>
|
||||
q;
|
||||
|
||||
const baseContext = ({
|
||||
mockedUseQueryBuilder.mockReturnValue(({
|
||||
currentQuery: currentQueryObj,
|
||||
stagedQuery: null,
|
||||
lastUsedQuery: null,
|
||||
@@ -134,10 +124,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
initQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(() => false),
|
||||
isDefaultQuery: jest.fn(() => false),
|
||||
} as unknown) as QueryBuilderContextType;
|
||||
|
||||
baseQBContext = baseContext;
|
||||
mockedUseQueryBuilder.mockReturnValue(baseQBContext);
|
||||
} as unknown) as QueryBuilderContextType);
|
||||
|
||||
mockedUseQueryOperations.mockReturnValue({
|
||||
isTracePanelType: false,
|
||||
@@ -152,7 +139,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
|
||||
handleDeleteQuery: jest.fn(),
|
||||
handleChangeQueryData: (jest.fn() as unknown) as ReturnType<UseQueryOperations>['handleChangeQueryData'],
|
||||
handleChangeFormulaData: jest.fn(),
|
||||
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
|
||||
handleQueryFunctionsUpdates: jest.fn(),
|
||||
listOfAdditionalFormulaFilters: [],
|
||||
});
|
||||
});
|
||||
@@ -212,56 +199,4 @@ 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
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
|
||||
}
|
||||
};
|
||||
@@ -1,369 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,173 +0,0 @@
|
||||
/* 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,5 +54,4 @@ export enum QueryParams {
|
||||
version = 'version',
|
||||
source = 'source',
|
||||
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
|
||||
isTestAlert = 'isTestAlert',
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.app-content {
|
||||
width: calc(100% - 54px); // width of the sidebar
|
||||
width: calc(100% - 64px); // 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({
|
||||
basic: {
|
||||
basicAlertState: {
|
||||
...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
|
||||
threshold: expect.any(Object),
|
||||
advancedOptions: expect.any(Object),
|
||||
evaluationWindow: expect.any(Object),
|
||||
notificationSettings: expect.any(Object),
|
||||
thresholdState: expect.any(Object),
|
||||
advancedOptionsState: expect.any(Object),
|
||||
evaluationWindowState: expect.any(Object),
|
||||
notificationSettingsState: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,11 +196,3 @@ 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,22 +17,26 @@ 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 {
|
||||
AdvancedOptionsAction,
|
||||
AlertThresholdAction,
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from './constants';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
CreateAlertAction,
|
||||
CreateAlertSlice,
|
||||
EvaluationWindowAction,
|
||||
ICreateAlertContextProps,
|
||||
ICreateAlertProviderProps,
|
||||
NotificationSettingsAction,
|
||||
} from './types';
|
||||
import {
|
||||
advancedOptionsReducer,
|
||||
alertCreationReducer,
|
||||
alertThresholdReducer,
|
||||
buildInitialAlertDef,
|
||||
createAlertReducer,
|
||||
evaluationWindowReducer,
|
||||
getInitialAlertTypeFromURL,
|
||||
notificationSettingsReducer,
|
||||
} from './utils';
|
||||
|
||||
const CreateAlertContext = createContext<ICreateAlertContextProps | null>(null);
|
||||
@@ -61,65 +65,10 @@ export function CreateAlertProvider(
|
||||
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
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 [alertState, setAlertState] = useReducer(alertCreationReducer, {
|
||||
...INITIAL_ALERT_STATE,
|
||||
yAxisUnit: currentQuery.unit,
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
@@ -155,56 +104,92 @@ 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(() => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'RESET',
|
||||
},
|
||||
setThresholdState({
|
||||
type: 'RESET',
|
||||
});
|
||||
|
||||
if (thresholdsFromURL) {
|
||||
try {
|
||||
const thresholds = JSON.parse(thresholdsFromURL);
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholds,
|
||||
},
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholds,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing thresholds from URL:', error);
|
||||
}
|
||||
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.EVALUATION_WINDOW,
|
||||
action: {
|
||||
type: 'SET_INITIAL_STATE_FOR_METER',
|
||||
},
|
||||
setEvaluationWindow({
|
||||
type: 'SET_INITIAL_STATE_FOR_METER',
|
||||
});
|
||||
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'SET_MATCH_TYPE',
|
||||
payload: AlertThresholdMatchType.IN_TOTAL,
|
||||
},
|
||||
setThresholdState({
|
||||
type: 'SET_MATCH_TYPE',
|
||||
payload: AlertThresholdMatchType.IN_TOTAL,
|
||||
});
|
||||
}
|
||||
}, [alertType, thresholdsFromURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && initialAlertState) {
|
||||
setCreateAlertState({
|
||||
setAlertState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}, [initialAlertState, isEditMode]);
|
||||
|
||||
const discardAlertRule = useCallback(() => {
|
||||
setCreateAlertState({
|
||||
setAlertState({
|
||||
type: 'RESET',
|
||||
});
|
||||
setThresholdState({
|
||||
type: 'RESET',
|
||||
});
|
||||
setEvaluationWindow({
|
||||
type: 'RESET',
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'RESET',
|
||||
});
|
||||
setNotificationSettings({
|
||||
type: 'RESET',
|
||||
});
|
||||
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
|
||||
@@ -227,17 +212,17 @@ export function CreateAlertProvider(
|
||||
|
||||
const contextValue: ICreateAlertContextProps = useMemo(
|
||||
() => ({
|
||||
alertState: createAlertState.basic,
|
||||
alertState,
|
||||
setAlertState,
|
||||
alertType,
|
||||
setAlertType: handleAlertTypeChange,
|
||||
thresholdState: createAlertState.threshold,
|
||||
thresholdState,
|
||||
setThresholdState,
|
||||
evaluationWindow: createAlertState.evaluationWindow,
|
||||
evaluationWindow,
|
||||
setEvaluationWindow,
|
||||
advancedOptions: createAlertState.advancedOptions,
|
||||
advancedOptions,
|
||||
setAdvancedOptions,
|
||||
notificationSettings: createAlertState.notificationSettings,
|
||||
notificationSettings,
|
||||
setNotificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
@@ -249,14 +234,13 @@ export function CreateAlertProvider(
|
||||
isEditMode: isEditMode || false,
|
||||
}),
|
||||
[
|
||||
createAlertState,
|
||||
setAlertState,
|
||||
setThresholdState,
|
||||
setEvaluationWindow,
|
||||
setAdvancedOptions,
|
||||
setNotificationSettings,
|
||||
alertState,
|
||||
alertType,
|
||||
handleAlertTypeChange,
|
||||
thresholdState,
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
notificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
isCreatingAlertRule,
|
||||
|
||||
@@ -9,6 +9,8 @@ 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>;
|
||||
@@ -50,7 +52,7 @@ export interface ICreateAlertContextProps {
|
||||
export interface ICreateAlertProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialAlertType: AlertTypes;
|
||||
initialAlertState?: CreateAlertState;
|
||||
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
|
||||
isEditMode?: boolean;
|
||||
ruleId?: string;
|
||||
}
|
||||
@@ -270,31 +272,3 @@ 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,7 +18,6 @@ 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';
|
||||
@@ -29,9 +28,6 @@ import {
|
||||
AlertThresholdAction,
|
||||
AlertThresholdState,
|
||||
CreateAlertAction,
|
||||
CreateAlertReducerAction,
|
||||
CreateAlertSlice,
|
||||
CreateAlertState,
|
||||
EvaluationWindowAction,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsAction,
|
||||
@@ -255,57 +251,3 @@ 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,
|
||||
): CreateAlertState {
|
||||
): GetCreateAlertLocalStateFromAlertDefReturn {
|
||||
if (!alertDef) {
|
||||
return {
|
||||
basic: INITIAL_ALERT_STATE,
|
||||
threshold: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
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 alert state
|
||||
@@ -294,10 +294,10 @@ export function getCreateAlertLocalStateFromAlertDef(
|
||||
);
|
||||
|
||||
return {
|
||||
basic: basicAlertState,
|
||||
threshold: thresholdState,
|
||||
advancedOptions: advancedOptionsState,
|
||||
evaluationWindow: evaluationWindowState,
|
||||
notificationSettings: notificationSettingsState,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptionsState,
|
||||
evaluationWindowState,
|
||||
notificationSettingsState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const listViewInitialLogQuery: Query = {
|
||||
},
|
||||
};
|
||||
|
||||
export const listViewInitialTraceQuery: Query = {
|
||||
export const listViewInitialTraceQuery = {
|
||||
// it should be the above commented query
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
|
||||
@@ -21,7 +21,6 @@ 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';
|
||||
@@ -45,7 +44,7 @@ import {
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
@@ -57,11 +56,7 @@ import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import { VariablesSettingsTab } from './types';
|
||||
import {
|
||||
DEFAULT_ROW_NAME,
|
||||
downloadObjectAsJson,
|
||||
sanitizeDashboardData,
|
||||
} from './utils';
|
||||
import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils';
|
||||
|
||||
import './Description.styles.scss';
|
||||
|
||||
@@ -69,6 +64,28 @@ 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();
|
||||
@@ -102,7 +119,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
uuid: selectedDashboard.id,
|
||||
}
|
||||
: ({} as DashboardData);
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { title = '', description, tags, image = Base64Icons[0] } =
|
||||
selectedData || {};
|
||||
@@ -560,7 +576,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
<section className="dashboard-description-section">{description}</section>
|
||||
)}
|
||||
|
||||
{!isEmpty(dashboardVariables) && (
|
||||
{!isEmpty(selectedData.variables) && (
|
||||
<section className="dashboard-variables">
|
||||
<DashboardVariableSelection />
|
||||
</section>
|
||||
|
||||
@@ -1,27 +1,3 @@
|
||||
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,8 +14,10 @@ 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 { useWidgetsByDynamicVariableId } from 'hooks/dashboard/useWidgetsByDynamicVariableId';
|
||||
import { getWidgetsHavingDynamicVariableAttribute } from 'hooks/dashboard/utils';
|
||||
import {
|
||||
createDynamicVariableToWidgetsMap,
|
||||
getWidgetsHavingDynamicVariableAttribute,
|
||||
} from 'hooks/dashboard/utils';
|
||||
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
@@ -241,11 +243,23 @@ function VariableItem({
|
||||
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
|
||||
|
||||
useEffect(() => {
|
||||
if (variableData?.id && variableData.id in widgetsByDynamicVariableId) {
|
||||
setSelectedWidgets(widgetsByDynamicVariableId[variableData.id] || []);
|
||||
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] || []);
|
||||
} else if (dynamicVariablesSelectedValue?.name) {
|
||||
const widgets = getWidgetsHavingDynamicVariableAttribute(
|
||||
dynamicVariablesSelectedValue?.name,
|
||||
@@ -261,7 +275,6 @@ function VariableItem({
|
||||
selectedDashboard,
|
||||
variableData.id,
|
||||
variableData.name,
|
||||
widgetsByDynamicVariableId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, 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,13 +17,11 @@ 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 { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { TVariableMode } from './types';
|
||||
import VariableItem from './VariableItem/VariableItem';
|
||||
@@ -93,10 +91,13 @@ 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<
|
||||
@@ -146,13 +147,13 @@ function VariablesSettings({
|
||||
const variableNamesMap = {};
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(dashboardVariables)) {
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const { order, id, name } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...dashboardVariables[key],
|
||||
...variables[key],
|
||||
id,
|
||||
});
|
||||
|
||||
@@ -173,10 +174,10 @@ function VariablesSettings({
|
||||
setVariablesTableData(tableRowData);
|
||||
setVariablesOrderArr(variableOrderArr);
|
||||
setExistingVariableNamesMap(variableNamesMap);
|
||||
}, [dashboardVariables]);
|
||||
}, [variables]);
|
||||
|
||||
const updateVariables = (
|
||||
updatedVariablesData: IDashboardVariables,
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
currentRequestedId?: string,
|
||||
widgetIds?: string[],
|
||||
applyToAll?: boolean,
|
||||
@@ -311,7 +312,7 @@ function VariablesSettings({
|
||||
currentVariableId?: string,
|
||||
): boolean => {
|
||||
// Check if any other dynamic variable already uses this attribute key
|
||||
const isDuplicateAttributeKey = Object.values(dashboardVariables).some(
|
||||
const isDuplicateAttributeKey = Object.values(variables).some(
|
||||
(variable: IDashboardVariable) =>
|
||||
variable.type === 'DYNAMIC' &&
|
||||
variable.dynamicVariablesAttribute === attributeKey &&
|
||||
@@ -421,7 +422,7 @@ function VariablesSettings({
|
||||
{variableViewMode ? (
|
||||
<VariableItem
|
||||
variableData={{ ...variableEditData } as IDashboardVariable}
|
||||
existingVariables={dashboardVariables}
|
||||
existingVariables={variables}
|
||||
onSave={onVariableSaveHandler}
|
||||
onCancel={onDoneVariableViewMode}
|
||||
validateName={validateVariableName}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { memo, useEffect } from 'react';
|
||||
import { memo, useEffect, useState } 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';
|
||||
@@ -15,7 +11,13 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import DynamicVariableSelection from './DynamicVariableSelection';
|
||||
import { onUpdateVariableNode } from './util';
|
||||
import {
|
||||
buildDependencies,
|
||||
buildDependencyGraph,
|
||||
buildParentDependencyGraph,
|
||||
IDependencyData,
|
||||
onUpdateVariableNode,
|
||||
} from './util';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
@@ -31,12 +33,14 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
|
||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const sortedVariablesArray = useDashboardVariablesSelector(
|
||||
(state) => state.sortedVariablesArray,
|
||||
);
|
||||
const dependencyData = useDashboardVariablesSelector(
|
||||
(state) => state.dependencyData,
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { variables } = data || {};
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
|
||||
const [dependencyData, setDependencyData] = useState<IDependencyData | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
@@ -44,13 +48,53 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize variables with default values if not in URL
|
||||
initializeDefaultVariables(
|
||||
dashboardVariables,
|
||||
getUrlVariables,
|
||||
updateUrlVariable,
|
||||
);
|
||||
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
|
||||
if (variables) {
|
||||
const tableRowData = [];
|
||||
|
||||
// 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
|
||||
@@ -78,7 +122,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
if (id) {
|
||||
// For dynamic variables, only store in localStorage when NOT allSelected
|
||||
// This makes localStorage much lighter by avoiding storing all individual values
|
||||
const variable = dashboardVariables?.[id] || dashboardVariables?.[name];
|
||||
const variable = variables?.[id] || variables?.[name];
|
||||
const isDynamic = variable?.type === 'DYNAMIC';
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
|
||||
|
||||
@@ -141,30 +185,45 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
}
|
||||
};
|
||||
|
||||
if (!variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orderBasedSortedVariables = variablesTableData.sort(
|
||||
(a: { order: number }, b: { order: number }) => a.order - b.order,
|
||||
);
|
||||
|
||||
return (
|
||||
<Row style={{ display: 'flex', gap: '12px' }}>
|
||||
{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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ 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 { 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';
|
||||
@@ -25,7 +24,7 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { ALL_SELECT_VALUE, variablePropsToPayloadVariables } from '../utils';
|
||||
import { SelectItemStyle } from './styles';
|
||||
import { areArraysEqual, checkAPIInvocation } from './util';
|
||||
import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
|
||||
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useCallback } from 'react';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
@@ -28,7 +27,7 @@ interface UseDashboardVariableUpdateReturn {
|
||||
widgetId?: string,
|
||||
) => void;
|
||||
updateVariables: (
|
||||
updatedVariablesData: IDashboardVariables,
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
currentRequestedId?: string,
|
||||
widgetIds?: string[],
|
||||
applyToAll?: boolean,
|
||||
@@ -107,7 +106,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
|
||||
const updateVariables = useCallback(
|
||||
(
|
||||
updatedVariablesData: IDashboardVariables,
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
currentRequestedId?: string,
|
||||
widgetIds?: string[],
|
||||
applyToAll?: boolean,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { OptionData } from 'components/NewSelect/types';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import {
|
||||
IDashboardVariables,
|
||||
IDependencyData,
|
||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function areArraysEqual(
|
||||
a: (string | number | boolean)[],
|
||||
@@ -25,7 +21,7 @@ export function areArraysEqual(
|
||||
|
||||
export const convertVariablesToDbFormat = (
|
||||
variblesArr: IDashboardVariable[],
|
||||
): IDashboardVariables =>
|
||||
): Dashboard['data']['variables'] =>
|
||||
variblesArr.reduce((result, obj: IDashboardVariable) => {
|
||||
const { id } = obj;
|
||||
|
||||
@@ -100,6 +96,14 @@ export const buildDependencies = (
|
||||
return graph;
|
||||
};
|
||||
|
||||
export interface IDependencyData {
|
||||
order: string[];
|
||||
graph: VariableGraph;
|
||||
parentDependencyGraph: VariableGraph;
|
||||
hasCycle: boolean;
|
||||
cycleNodes?: string[];
|
||||
}
|
||||
|
||||
export const buildParentDependencyGraph = (
|
||||
graph: VariableGraph,
|
||||
): VariableGraph => {
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
@@ -1,133 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 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',
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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,7 +2,6 @@
|
||||
|
||||
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';
|
||||
@@ -157,7 +156,6 @@ describe('PanelWrapper with DragSelect', () => {
|
||||
|
||||
render(
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.STANDALONE_VIEW}
|
||||
widget={mockWidget}
|
||||
queryResponse={mockQueryResponse}
|
||||
onDragSelect={mockOnDragSelect}
|
||||
|
||||
@@ -17,7 +17,6 @@ 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 {
|
||||
@@ -26,7 +25,6 @@ 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';
|
||||
@@ -81,7 +79,6 @@ function FullView({
|
||||
}, [setCurrentGraphRef]);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const { user } = useAppContext();
|
||||
|
||||
const [editWidget] = useComponentPermission(['edit_widget'], user.role);
|
||||
@@ -117,7 +114,7 @@ function FullView({
|
||||
graphType: getGraphType(selectedPanelType),
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: selectedPanelType === PANEL_TYPES.TABLE,
|
||||
originalGraphType: selectedPanelType,
|
||||
@@ -128,7 +125,7 @@ function FullView({
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
@@ -367,7 +364,6 @@ function FullView({
|
||||
/>
|
||||
)}
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.STANDALONE_VIEW}
|
||||
queryResponse={response}
|
||||
widget={widget}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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';
|
||||
@@ -408,7 +407,6 @@ function WidgetGraphComponent({
|
||||
ref={graphRef}
|
||||
>
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_VIEW}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -53,7 +53,7 @@ function GridCardGraph({
|
||||
customOnRowClick,
|
||||
customTimeRangeWindowForCoRelation,
|
||||
enableDrillDown,
|
||||
widgetsByDynamicVariableId,
|
||||
widgetsHavingDynamicVariables,
|
||||
}: 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' ||
|
||||
(widgetsByDynamicVariableId?.[variable.id] &&
|
||||
widgetsByDynamicVariableId?.[variable.id].includes(widget.id))
|
||||
(widgetsHavingDynamicVariables?.[variable.id] &&
|
||||
widgetsHavingDynamicVariables?.[variable.id].includes(widget.id))
|
||||
) {
|
||||
return { ...acc, [id]: variable.selectedValue };
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ 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 { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import uPlot from 'uplot';
|
||||
@@ -51,7 +50,7 @@ export interface GridCardGraphProps {
|
||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
isQueryEnabled: boolean;
|
||||
variables?: IDashboardVariables;
|
||||
variables?: Dashboard['data']['variables'];
|
||||
version?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
customOnDragSelect?: (start: number, end: number) => void;
|
||||
@@ -72,7 +71,7 @@ export interface GridCardGraphProps {
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||
enableDrillDown?: boolean;
|
||||
widgetsByDynamicVariableId?: Record<string, string[]>;
|
||||
widgetsHavingDynamicVariables?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -14,9 +14,8 @@ 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 { useWidgetsByDynamicVariableId } from 'hooks/dashboard/useWidgetsByDynamicVariableId';
|
||||
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -35,7 +34,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 { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IDashboardVariable, 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';
|
||||
@@ -80,9 +79,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { widgets } = data || {};
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const { widgets, variables } = data || {};
|
||||
|
||||
const { user } = useAppContext();
|
||||
|
||||
@@ -102,7 +99,21 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
Record<string, { widgets: Layout[]; collapsed: boolean }>
|
||||
>({});
|
||||
|
||||
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPanelMap(panelMap);
|
||||
@@ -167,11 +178,11 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardName: data.title,
|
||||
numberOfPanels: data.widgets?.length,
|
||||
numberOfVariables: Object.keys(dashboardVariables).length || 0,
|
||||
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [dashboardVariables, data, selectedDashboard?.id]);
|
||||
}, [data, selectedDashboard?.id]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) {
|
||||
@@ -611,13 +622,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
<GridCard
|
||||
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||
headerMenuList={widgetActions}
|
||||
variables={dashboardVariables}
|
||||
variables={variables}
|
||||
// version={selectedDashboard?.data?.version}
|
||||
version={ENTITY_VERSION_V5}
|
||||
onDragSelect={onDragSelect}
|
||||
dataAvailable={checkIfDataExists}
|
||||
enableDrillDown={enableDrillDown}
|
||||
widgetsByDynamicVariableId={widgetsByDynamicVariableId}
|
||||
widgetsHavingDynamicVariables={widgetsHavingDynamicVariables}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } 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';
|
||||
@@ -35,9 +36,14 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
|
||||
const queryRangeMutation = useMutation(getSubstituteVars);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const dynamicVariables = useMemo(
|
||||
() =>
|
||||
Object.values(selectedDashboard?.data?.variables || {})?.filter(
|
||||
(variable: IDashboardVariable) => variable.type === 'DYNAMIC',
|
||||
),
|
||||
[selectedDashboard],
|
||||
);
|
||||
|
||||
const getUpdatedQuery = useCallback(
|
||||
@@ -53,7 +59,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(selectedDashboard?.data?.variables),
|
||||
originalGraphType: widgetConfig.panelTypes,
|
||||
dynamicVariables: dashboardDynamicVariables,
|
||||
dynamicVariables,
|
||||
});
|
||||
|
||||
// Execute query and process results
|
||||
@@ -62,7 +68,7 @@ function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
// Map query data from API response
|
||||
return mapQueryDataFromApi(queryResult.data.compositeQuery);
|
||||
},
|
||||
[dashboardDynamicVariables, globalSelectedInterval, queryRangeMutation],
|
||||
[dynamicVariables, globalSelectedInterval, queryRangeMutation],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 54px);
|
||||
width: calc(100% - 64px);
|
||||
background: rgb(18, 19, 23);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
@@ -442,7 +442,7 @@
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 54px);
|
||||
width: calc(100% - 64px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
height: calc(100% - 54px);
|
||||
height: calc(100% - 64px);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 1rem;
|
||||
|
||||
@@ -39,10 +39,8 @@ import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
downloadObjectAsJson,
|
||||
sanitizeDashboardData,
|
||||
} from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { sanitizeDashboardData } from 'container/DashboardContainer/DashboardDescription';
|
||||
import { downloadObjectAsJson } 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(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
max-width: 317px;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
@@ -51,8 +51,8 @@
|
||||
|
||||
.login-form-card {
|
||||
width: 100%;
|
||||
background: var(--bg-neutral-dark-950);
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
@@ -174,12 +174,12 @@
|
||||
|
||||
&__docs-button {
|
||||
color: var(--bg-ink-400);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-100);
|
||||
border-color: var(--bg-vanilla-400);
|
||||
border-color: var(--bg-vanilla-400, #d1d5db);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@@ -230,8 +230,8 @@
|
||||
|
||||
.login-form-input {
|
||||
height: 32px;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
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(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@
|
||||
font-weight: 400 !important;
|
||||
line-height: 1 !important;
|
||||
letter-spacing: -0.065px !important;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
@@ -296,19 +296,19 @@
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--l3-border) !important;
|
||||
border-color: var(--levels-l3-border, #2c303a) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--primary) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
@@ -339,26 +339,26 @@
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--primary);
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--bg-neutral-dark-50);
|
||||
color: var(--semantic-primary-foreground, #eceef2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary);
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--primary);
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -370,12 +370,12 @@
|
||||
}
|
||||
|
||||
.login-form-description {
|
||||
color: var(--text-neutral-light-200);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.login-form-card {
|
||||
background: var(--bg-base-white);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
@@ -383,36 +383,36 @@
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
background: var(--bg-vanilla-200);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-ink-500);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
|
||||
// Select component light mode styling
|
||||
&.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--primary) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Label = styled.label`
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
@@ -47,29 +47,29 @@ export const FormContainer = styled(Form)`
|
||||
& .ant-input,
|
||||
& .ant-input-password,
|
||||
& .ant-select-selector {
|
||||
background: var(--l3-background) !important;
|
||||
border-color: var(--l3-border) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border-color: var(--levels-l3-border, #2c303a) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
|
||||
.lightMode & {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input:focus,
|
||||
& .ant-input-password:focus,
|
||||
& .ant-select-focused .ant-select-selector {
|
||||
border-color: var(--primary) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
.ant-pagination {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: calc(100% - 54px);
|
||||
width: calc(100% - 64px);
|
||||
background: var(--bg-ink-500);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
.dashboard-navigation {
|
||||
.run-query-dashboard-btn {
|
||||
min-width: 180px;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
border: none !important;
|
||||
margin-left: 0px !important;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -36,11 +35,8 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
|
||||
import './QuerySection.styles.scss';
|
||||
function QuerySection({
|
||||
selectedGraph,
|
||||
queryRangeKey,
|
||||
isLoadingQueries,
|
||||
}: QueryProps): JSX.Element {
|
||||
|
||||
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
handleRunQuery: handleRunQueryFromQueryBuilder,
|
||||
@@ -168,7 +164,6 @@ function QuerySection({
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
queryComponents={queryComponents}
|
||||
signalSourceChangeEnabled
|
||||
savePreviousQuery
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -242,13 +237,7 @@ function QuerySection({
|
||||
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
|
||||
className="run-query-dashboard-btn"
|
||||
label="Stage & Run Query"
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
queryRangeKey={queryRangeKey}
|
||||
/>
|
||||
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
@@ -259,8 +248,6 @@ function QuerySection({
|
||||
|
||||
interface QueryProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
queryRangeKey?: QueryKey;
|
||||
isLoadingQueries?: boolean;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
@@ -184,7 +183,6 @@ function WidgetGraph({
|
||||
}}
|
||||
>
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_EDIT}
|
||||
widget={selectedWidget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -25,8 +24,8 @@ function LeftContainer({
|
||||
setSelectedTracesFields,
|
||||
selectedWidget,
|
||||
requestData,
|
||||
isLoadingPanelData,
|
||||
setRequestData,
|
||||
isLoadingPanelData,
|
||||
setQueryResponse,
|
||||
enableDrillDown = false,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
@@ -36,20 +35,15 @@ function LeftContainer({
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const queryRangeKey = useMemo(
|
||||
() => [
|
||||
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
|
||||
enabled: !!stagedQuery,
|
||||
queryKey: [
|
||||
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
|
||||
@@ -70,11 +64,7 @@ function LeftContainer({
|
||||
enableDrillDown={enableDrillDown}
|
||||
/>
|
||||
<QueryContainer className="query-section-left-container">
|
||||
<QuerySection
|
||||
selectedGraph={selectedGraph}
|
||||
queryRangeKey={queryRangeKey}
|
||||
isLoadingQueries={queryResponse.isFetching}
|
||||
/>
|
||||
<QuerySection selectedGraph={selectedGraph} />
|
||||
{selectedGraph === PANEL_TYPES.LIST && (
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={selectedLogFields}
|
||||
|
||||
@@ -26,7 +26,6 @@ import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||
import GraphTypes, {
|
||||
ItemsProps,
|
||||
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
Spline,
|
||||
SquareArrowOutUpRight,
|
||||
} from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ColumnUnit,
|
||||
@@ -131,7 +131,7 @@ function RightContainer({
|
||||
enableDrillDown = false,
|
||||
isNewDashboard,
|
||||
}: RightContainerProps): JSX.Element {
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const [inputValue, setInputValue] = useState(title);
|
||||
const [autoCompleteOpen, setAutoCompleteOpen] = useState(false);
|
||||
const [cursorPos, setCursorPos] = useState(0);
|
||||
@@ -173,12 +173,16 @@ function RightContainer({
|
||||
|
||||
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
|
||||
|
||||
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
||||
return Object.entries(dashboardVariables).map(([, value]) => ({
|
||||
// Get dashboard variables
|
||||
const dashboardVariables = useMemo<VariableOption[]>(() => {
|
||||
if (!selectedDashboard?.data?.variables) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(selectedDashboard.data.variables).map(([, value]) => ({
|
||||
value: value.name || '',
|
||||
label: value.name || '',
|
||||
}));
|
||||
}, [dashboardVariables]);
|
||||
}, [selectedDashboard?.data?.variables]);
|
||||
|
||||
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
||||
setCursorPos(pos);
|
||||
@@ -270,7 +274,7 @@ function RightContainer({
|
||||
<section className="name-description">
|
||||
<Typography.Text className="typography">Name</Typography.Text>
|
||||
<AutoComplete
|
||||
options={dashboardVariableOptions}
|
||||
options={dashboardVariables}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onSelect={onSelect}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
|
||||
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -90,8 +89,6 @@ function NewWidget({
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -380,7 +377,7 @@ function NewWidget({
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
|
||||
globalSelectedInterval: customGlobalSelectedInterval,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
@@ -397,7 +394,7 @@ function NewWidget({
|
||||
formatForWeb:
|
||||
getGraphTypeForFormat(selectedGraph || selectedWidget.panelTypes) ===
|
||||
PANEL_TYPES.TABLE,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
originalGraphType: selectedGraph || selectedWidget?.panelTypes,
|
||||
};
|
||||
}
|
||||
@@ -411,7 +408,7 @@ function NewWidget({
|
||||
graphType: selectedGraph,
|
||||
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
|
||||
globalSelectedInterval: customGlobalSelectedInterval,
|
||||
variables: getDashboardVariables(dashboardVariables),
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.table-header-cell {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
@@ -111,9 +111,9 @@
|
||||
|
||||
height: 32px !important;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l3-border);
|
||||
color: var(--l1-foreground);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
@@ -123,15 +123,15 @@
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border);
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -142,9 +142,9 @@
|
||||
.ant-select-selector {
|
||||
height: 32px !important;
|
||||
border-radius: 2px !important;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
@@ -154,25 +154,25 @@
|
||||
box-sizing: border-box !important;
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bg-robin-500);
|
||||
outline: 2px solid var(--semantic-primary-background, #4e74f8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -247,9 +247,9 @@
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px dashed var(--l2-border) !important;
|
||||
border: 1px dashed var(--semantic-secondary-border, #23262e) !important;
|
||||
background: transparent !important;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -263,38 +263,38 @@
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
button,
|
||||
& {
|
||||
border: 1px dashed var(--l2-border) !important;
|
||||
border: 1px dashed var(--semantic-secondary-border, #23262e) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-style: dashed !important;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
|
||||
button,
|
||||
& {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-style: dashed !important;
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bg-robin-500);
|
||||
outline: 2px solid var(--semantic-primary-background, #4e74f8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -327,7 +327,7 @@
|
||||
gap: 8px;
|
||||
|
||||
.success-message {
|
||||
color: var(--bg-success-500);
|
||||
color: var(--bg-success-500, #00b37e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@
|
||||
gap: 8px;
|
||||
|
||||
.partially-sent-invites-message {
|
||||
color: var(--bg-warning-500);
|
||||
color: var(--bg-warning-500, #fbbd23);
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
@@ -358,110 +358,110 @@
|
||||
.lightMode {
|
||||
.invite-team-members-table-header {
|
||||
.table-header-cell {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--semantic-secondary-foreground, #747b8b);
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500, #1a1d26) !important;
|
||||
|
||||
input {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500, #1a1d26) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-role-select {
|
||||
.ant-select-selector {
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
background: var(--levels-l3-background, #ffffff) !important;
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-team-member-button {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 101, 101, 0.1) !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-another-member-button {
|
||||
border: 1px dashed var(--text-vanilla-300) !important;
|
||||
border: 1px dashed var(--semantic-secondary-border, #e9e9e9) !important;
|
||||
background: transparent !important;
|
||||
color: var(--l3-foreground);
|
||||
color: var(--semantic-secondary-foreground, #747b8b);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--semantic-secondary-foreground, #747b8b) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-style: dashed !important;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,7 +470,7 @@
|
||||
.invite-users-error-message-container,
|
||||
.invite-users-success-message-container {
|
||||
.success-message {
|
||||
color: var(--bg-success-500);
|
||||
color: var(--bg-success-500, #00b37e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
.partially-sent-invites-message {
|
||||
color: var(--bg-warning-500);
|
||||
color: var(--bg-warning-500, #fbbd23);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -60,7 +60,7 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
max-width: 528px;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
@@ -99,8 +99,8 @@
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--bg-neutral-dark-950);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
@@ -119,9 +119,9 @@
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
resize: none;
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
@@ -132,13 +132,13 @@
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +164,8 @@
|
||||
font-size: 12px;
|
||||
|
||||
height: 32px;
|
||||
background: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--Ink-300, #16181d);
|
||||
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
|
||||
@@ -173,8 +173,8 @@
|
||||
font-size: 11px;
|
||||
height: 32px;
|
||||
min-width: 80px;
|
||||
background: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--Ink-300, #16181d);
|
||||
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
|
||||
border-left: 0px;
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
@@ -182,7 +182,7 @@
|
||||
}
|
||||
|
||||
.question-label {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
@@ -208,8 +208,8 @@
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--l2-border);
|
||||
color: var(--l2-foreground);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -219,7 +219,7 @@
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -236,9 +236,9 @@
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-robin-500);
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
border: none;
|
||||
color: var(--bg-neutral-dark-50);
|
||||
color: var(--semantic-primary-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -267,7 +267,7 @@
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -300,7 +300,7 @@
|
||||
margin-top: 12px;
|
||||
|
||||
.ant-slider-mark-text {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
|
||||
@@ -343,7 +343,7 @@
|
||||
|
||||
.question {
|
||||
font-family: Inter, sans-serif;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
@@ -358,7 +358,7 @@
|
||||
}
|
||||
|
||||
.question-slider {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
@@ -381,18 +381,18 @@
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,7 +499,7 @@
|
||||
width: calc((528px - 12px) / 2);
|
||||
min-width: 258px;
|
||||
flex: 0 0 calc((528px - 12px) / 2);
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
@@ -511,12 +511,12 @@
|
||||
.ant-radio-inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-color: var(--l3-border);
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +541,7 @@
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
|
||||
&.checkbox-item-others {
|
||||
@@ -553,9 +553,9 @@
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
@@ -566,18 +566,18 @@
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-robin-500);
|
||||
color: var(--l1-foreground);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
@@ -592,19 +592,19 @@
|
||||
.ant-checkbox-inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1.5px solid var(--l3-background);
|
||||
border: 1.5px solid var(--levels-l3-background, #23262e);
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -646,7 +646,7 @@
|
||||
|
||||
.add-another-member-button,
|
||||
.remove-team-member-button {
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
text-align: center;
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-family: Inter;
|
||||
@@ -735,11 +735,11 @@
|
||||
}
|
||||
|
||||
.onboarding-header-title {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
|
||||
.onboarding-header-subtitle {
|
||||
color: var(--l3-foreground) !important;
|
||||
color: var(--semantic-secondary-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.questions-form {
|
||||
@@ -750,8 +750,8 @@
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--text-vanilla-300);
|
||||
background: var(--text-base-white);
|
||||
border: 1px solid var(--semantic-secondary-border, #e9e9e9);
|
||||
background: var(--semantic-secondary-background, #ffffff);
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
@@ -767,18 +767,18 @@
|
||||
}
|
||||
|
||||
.discover-signoz-input {
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
font-weight: 400;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -808,51 +808,51 @@
|
||||
}
|
||||
|
||||
.question {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
|
||||
.question-slider {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
label {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
.ant-checkbox {
|
||||
.ant-checkbox-inner {
|
||||
border-color: var(--l3-background);
|
||||
border-color: var(--levels-l3-background, #ffffff);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--l1-foreground) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.checkbox-item-others {
|
||||
.onboarding-questionaire-other-input {
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-robin-500);
|
||||
color: var(--l1-foreground);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -860,32 +860,32 @@
|
||||
|
||||
.observability-tool-others-item {
|
||||
.onboarding-questionaire-other-input {
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-robin-500);
|
||||
color: var(--l1-foreground);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
border: 1px solid var(--l3-border);
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l3-foreground, #1a1d26);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -981,16 +981,16 @@
|
||||
.opentelemetry-radio-group {
|
||||
.opentelemetry-radio-items-wrapper {
|
||||
.opentelemetry-radio-item {
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
.ant-radio {
|
||||
.ant-radio-inner {
|
||||
border-color: var(--l3-border);
|
||||
border-color: var(--levels-l3-border, #e9e9e9);
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -999,11 +999,11 @@
|
||||
}
|
||||
|
||||
.onboarding-back-button {
|
||||
border-color: var(--text-vanilla-300);
|
||||
color: var(--l3-foreground);
|
||||
border-color: var(--semantic-secondary-border, #e9e9e9);
|
||||
color: var(--semantic-secondary-foreground, #747b8b);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-robin-500);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ function PanelWrapper({
|
||||
onOpenTraceBtnClick,
|
||||
customSeries,
|
||||
customOnRowClick,
|
||||
panelMode,
|
||||
enableDrillDown = false,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const Component = PanelTypeVsPanelWrapper[
|
||||
@@ -34,7 +33,6 @@ function PanelWrapper({
|
||||
}
|
||||
return (
|
||||
<Component
|
||||
panelMode={panelMode}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
@@ -12,7 +11,6 @@ describe('Table panel wrappper tests', () => {
|
||||
it('table should render fine with the query response and column units', () => {
|
||||
const { container, getByText } = render(
|
||||
<TablePanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_VIEW}
|
||||
widget={(tablePanelWidgetQuery as unknown) as Widgets}
|
||||
queryResponse={(tablePanelQueryResponse as unknown) as any}
|
||||
onDragSelect={(): void => {}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
@@ -21,7 +20,6 @@ describe('Value panel wrappper tests', () => {
|
||||
it('should render value panel correctly with yaxis unit', () => {
|
||||
const { getByText } = render(
|
||||
<ValuePanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_VIEW}
|
||||
widget={(valuePanelWidget as unknown) as Widgets}
|
||||
queryResponse={(valuePanelQueryResponse as unknown) as any}
|
||||
onDragSelect={(): void => {}}
|
||||
@@ -36,7 +34,6 @@ describe('Value panel wrappper tests', () => {
|
||||
it('should render tooltip when there are conflicting thresholds', () => {
|
||||
const { getByTestId, container } = render(
|
||||
<ValuePanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_VIEW}
|
||||
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
|
||||
queryResponse={(valuePanelQueryResponse as unknown) as any}
|
||||
onDragSelect={(): void => {}}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { WidgetGraphComponentProps } from 'container/GridCardLayout/GridCard/types';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
@@ -32,7 +31,6 @@ export type PanelWrapperProps = {
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||
enableDrillDown?: boolean;
|
||||
panelMode: PanelMode;
|
||||
};
|
||||
|
||||
export type TooltipData = {
|
||||
|
||||
@@ -37,7 +37,6 @@ export type QueryBuilderProps = {
|
||||
onChangeTraceView?: (view: TraceView) => void;
|
||||
onSignalSourceChange?: (value: string) => void;
|
||||
signalSourceChangeEnabled?: boolean;
|
||||
savePreviousQuery?: boolean;
|
||||
};
|
||||
|
||||
export enum TraceView {
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
pointer-events: none;
|
||||
// line-height: 18px;
|
||||
|
||||
color: var(--bg-sakura-400) !important;
|
||||
|
||||
@@ -96,7 +96,6 @@ export default function QueryFunctions({
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const hasAnomalyFunction = functions.some((func) => func.name === 'anomaly');
|
||||
const hasFunctions = functions.length > 0;
|
||||
|
||||
const handleAddNewFunction = (): void => {
|
||||
const defaultFunctionStruct =
|
||||
@@ -181,14 +180,10 @@ export default function QueryFunctions({
|
||||
<div
|
||||
className={cx(
|
||||
'query-functions-container',
|
||||
hasFunctions ? 'hasFunctions' : '',
|
||||
functions && functions.length > 0 ? 'hasFunctions' : '',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className="periscope-btn function-btn"
|
||||
disabled={hasFunctions}
|
||||
onClick={handleAddNewFunction}
|
||||
>
|
||||
<Button className="periscope-btn function-btn">
|
||||
<FunctionIcon
|
||||
className="function-icon"
|
||||
fillColor={!isDarkMode ? '#0B0C0E' : 'white'}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { QueryKey, useIsFetching, useQueryClient } from 'react-query';
|
||||
import { Button } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ChevronUp,
|
||||
Command,
|
||||
@@ -12,56 +9,35 @@ import {
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
import './RunQueryBtn.scss';
|
||||
|
||||
interface RunQueryBtnProps {
|
||||
className?: string;
|
||||
label?: string;
|
||||
isLoadingQueries?: boolean;
|
||||
handleCancelQuery?: () => void;
|
||||
onStageRunQuery?: () => void;
|
||||
queryRangeKey?: QueryKey;
|
||||
}
|
||||
|
||||
function RunQueryBtn({
|
||||
className,
|
||||
label,
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
onStageRunQuery,
|
||||
queryRangeKey,
|
||||
}: RunQueryBtnProps): JSX.Element {
|
||||
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
|
||||
const queryClient = useQueryClient();
|
||||
const isKeyFetchingCount = useIsFetching(
|
||||
queryRangeKey as QueryKey | undefined,
|
||||
);
|
||||
const isLoading =
|
||||
typeof isLoadingQueries === 'boolean'
|
||||
? isLoadingQueries
|
||||
: isKeyFetchingCount > 0;
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
if (handleCancelQuery) {
|
||||
return handleCancelQuery();
|
||||
}
|
||||
if (queryRangeKey) {
|
||||
queryClient.cancelQueries(queryRangeKey);
|
||||
}
|
||||
}, [handleCancelQuery, queryClient, queryRangeKey]);
|
||||
|
||||
return isLoading ? (
|
||||
return isLoadingQueries ? (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
||||
className={cx('cancel-query-btn periscope-btn danger', className)}
|
||||
onClick={onCancel}
|
||||
className="cancel-query-btn periscope-btn danger"
|
||||
onClick={handleCancelQuery}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className={cx('run-query-btn periscope-btn primary', className)}
|
||||
disabled={isLoading || !onStageRunQuery}
|
||||
className="run-query-btn periscope-btn primary"
|
||||
disabled={isLoadingQueries || !onStageRunQuery}
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
|
||||
@@ -3,16 +3,6 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import RunQueryBtn from '../RunQueryBtn';
|
||||
|
||||
jest.mock('react-query', () => {
|
||||
const actual = jest.requireActual('react-query');
|
||||
return {
|
||||
...actual,
|
||||
useIsFetching: jest.fn(),
|
||||
useQueryClient: jest.fn(),
|
||||
};
|
||||
});
|
||||
import { useIsFetching, useQueryClient } from 'react-query';
|
||||
|
||||
// Mock OS util
|
||||
jest.mock('utils/getUserOS', () => ({
|
||||
getUserOperatingSystem: jest.fn(),
|
||||
@@ -21,43 +11,10 @@ jest.mock('utils/getUserOS', () => ({
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
describe('RunQueryBtn', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
test('renders run state and triggers on click', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
(useIsFetching as jest.Mock).mockReturnValue(0);
|
||||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
cancelQueries: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
test('uses isLoadingQueries prop over useIsFetching', () => {
|
||||
// Simulate fetching but prop forces not loading
|
||||
(useIsFetching as jest.Mock).mockReturnValue(1);
|
||||
const onRun = jest.fn();
|
||||
render(<RunQueryBtn onStageRunQuery={onRun} isLoadingQueries={false} />);
|
||||
// Should show "Run Query" (not cancel)
|
||||
const runBtn = screen.getByRole('button', { name: /run query/i });
|
||||
expect(runBtn).toBeInTheDocument();
|
||||
expect(runBtn).toBeEnabled();
|
||||
});
|
||||
|
||||
test('fallback cancel: uses handleCancelQuery when no key provided', () => {
|
||||
(useIsFetching as jest.Mock).mockReturnValue(0);
|
||||
const cancelQueries = jest.fn();
|
||||
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
|
||||
|
||||
const onCancel = jest.fn();
|
||||
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
|
||||
|
||||
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
|
||||
fireEvent.click(cancelBtn);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
expect(cancelQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders run state and triggers on click', () => {
|
||||
const onRun = jest.fn();
|
||||
render(<RunQueryBtn onStageRunQuery={onRun} />);
|
||||
const btn = screen.getByRole('button', { name: /run query/i });
|
||||
@@ -67,11 +24,17 @@ describe('RunQueryBtn', () => {
|
||||
});
|
||||
|
||||
test('disabled when onStageRunQuery is undefined', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
render(<RunQueryBtn />);
|
||||
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('shows cancel state and calls handleCancelQuery', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const onCancel = jest.fn();
|
||||
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
|
||||
const cancel = screen.getByRole('button', { name: /cancel/i });
|
||||
@@ -79,24 +42,10 @@ describe('RunQueryBtn', () => {
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('derives loading from queryKey via useIsFetching and cancels via queryClient', () => {
|
||||
(useIsFetching as jest.Mock).mockReturnValue(1);
|
||||
const cancelQueries = jest.fn();
|
||||
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
|
||||
|
||||
const queryKey = ['GET_QUERY_RANGE', '1h', { some: 'req' }, 1, 2];
|
||||
render(<RunQueryBtn queryRangeKey={queryKey} />);
|
||||
|
||||
// Button switches to cancel state
|
||||
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
|
||||
expect(cancelBtn).toBeInTheDocument();
|
||||
|
||||
// Clicking cancel calls cancelQueries with the key
|
||||
fireEvent.click(cancelBtn);
|
||||
expect(cancelQueries).toHaveBeenCalledWith(queryKey);
|
||||
});
|
||||
|
||||
test('shows Command + CornerDownLeft on mac', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const { container } = render(
|
||||
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
||||
);
|
||||
@@ -121,6 +70,9 @@ describe('RunQueryBtn', () => {
|
||||
});
|
||||
|
||||
test('renders custom label when provided', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const onRun = jest.fn();
|
||||
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
|
||||
expect(
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
@@ -39,7 +38,9 @@ import {
|
||||
unset,
|
||||
} from 'lodash-es';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
@@ -247,9 +248,14 @@ function QueryBuilderSearchV2(
|
||||
return false;
|
||||
}, [currentState, query.aggregateAttribute?.dataType, query.dataSource]);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const dynamicVariables = useMemo(
|
||||
() =>
|
||||
Object.values(selectedDashboard?.data?.variables || {})?.filter(
|
||||
(variable: IDashboardVariable) => variable.type === 'DYNAMIC',
|
||||
),
|
||||
[selectedDashboard],
|
||||
);
|
||||
|
||||
const { data, isFetching } = useGetAggregateKeys(
|
||||
@@ -800,7 +806,7 @@ function QueryBuilderSearchV2(
|
||||
values.push(...(attributeValues?.payload?.[key] || []));
|
||||
|
||||
// here we want to suggest the variable name matching with the key here, we will go over the dynamic variables for the keys
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
const variableName = dynamicVariables?.find(
|
||||
(variable) =>
|
||||
variable?.dynamicVariablesAttribute === currentFilterItem?.key?.key,
|
||||
)?.name;
|
||||
@@ -831,7 +837,7 @@ function QueryBuilderSearchV2(
|
||||
suggestionsData?.payload?.attributes,
|
||||
operatorConfigKey,
|
||||
currentFilterItem?.key?.key,
|
||||
dashboardDynamicVariables,
|
||||
dynamicVariables,
|
||||
]);
|
||||
|
||||
// keep the query in sync with the selected tags in logs explorer page
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
} from 'constants/queryBuilder';
|
||||
import { IUseDashboardVariablesReturn } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -147,23 +145,27 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock dashboard variables
|
||||
const dashboardVariables = {
|
||||
service: {
|
||||
id: 'service',
|
||||
name: 'service',
|
||||
type: 'DYNAMIC' as IDashboardVariable['type'],
|
||||
dynamicVariablesAttribute: 'service.name',
|
||||
description: '',
|
||||
sort: 'DISABLED' as IDashboardVariable['sort'],
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
// Mock dashboard provider with dynamic variables
|
||||
const mockDashboard = {
|
||||
data: {
|
||||
variables: {
|
||||
service: {
|
||||
id: 'service',
|
||||
name: 'service',
|
||||
type: 'DYNAMIC',
|
||||
dynamicVariablesAttribute: 'service.name',
|
||||
description: '',
|
||||
sort: 'DISABLED',
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
|
||||
useDashboardVariables: (): IUseDashboardVariablesReturn => ({
|
||||
dashboardVariables: dashboardVariables,
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
selectedDashboard: mockDashboard,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { ArrowLeft, Plus, Settings, X } from 'lucide-react';
|
||||
import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
// import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -33,9 +33,17 @@ const useDashboardVarConfig = ({
|
||||
};
|
||||
// contextItems: React.ReactNode;
|
||||
} => {
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType('DYNAMIC');
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { onValueUpdate, createVariable } = useDashboardVariableUpdate();
|
||||
|
||||
const dynamicDashboardVariables = useMemo(
|
||||
(): [string, IDashboardVariable][] =>
|
||||
Object.entries(selectedDashboard?.data?.variables || {}).filter(
|
||||
([, value]) => value.name && value.type === 'DYNAMIC',
|
||||
),
|
||||
[selectedDashboard],
|
||||
);
|
||||
|
||||
// Function to determine the source from query data
|
||||
const getSourceFromQuery = useCallback(():
|
||||
| 'logs'
|
||||
@@ -108,7 +116,7 @@ const useDashboardVarConfig = ({
|
||||
<>
|
||||
{' '}
|
||||
{Object.entries(fieldVariables).map(([fieldName, value]) => {
|
||||
const dashboardVar = dashboardDynamicVariables.find(
|
||||
const dashboardVar = dynamicDashboardVariables.find(
|
||||
([, dynamicValue]) =>
|
||||
dynamicValue.dynamicVariablesAttribute === fieldName,
|
||||
);
|
||||
@@ -170,7 +178,7 @@ const useDashboardVarConfig = ({
|
||||
),
|
||||
[
|
||||
fieldVariables,
|
||||
dashboardDynamicVariables,
|
||||
dynamicDashboardVariables,
|
||||
handleSetVariable,
|
||||
handleUnsetVariable,
|
||||
handleCreateVariable,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--l1-foreground);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -48,12 +48,12 @@
|
||||
margin-top: 8px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-neutral-dark-950);
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
color: var(--l2-foreground);
|
||||
color: var(--semantic-secondary-foreground);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
.reset-password-form-container {
|
||||
width: 100%;
|
||||
background: var(--bg-neutral-dark-950);
|
||||
border: 1px solid var(--l3-background);
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
&.ant-input-affix-wrapper {
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
background: var(--l3-background);
|
||||
border-color: var(--l3-border);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
@@ -149,27 +149,27 @@
|
||||
}
|
||||
|
||||
.reset-password-header-subtitle {
|
||||
color: var(--text-neutral-light-200);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.reset-password-version-badge {
|
||||
background: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--text-neutral-light-200);
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-password-form {
|
||||
.reset-password-form-container {
|
||||
background: var(--bg-base-white);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
|
||||
|
||||
.reset-password-form-input {
|
||||
&.ant-input,
|
||||
&.ant-input-password,
|
||||
&.ant-input-affix-wrapper {
|
||||
background: var(--bg-vanilla-200);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
@@ -181,11 +181,11 @@
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
.nav-item-active-marker {
|
||||
background: #4e74f8;
|
||||
}
|
||||
|
||||
.nav-item-data {
|
||||
.nav-item-label {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@@ -33,14 +27,14 @@
|
||||
|
||||
.nav-item-data {
|
||||
color: white;
|
||||
background: var(--bg-slate-500, #161922);
|
||||
background: var(--Slate-500, #161922);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.nav-item-data {
|
||||
color: white;
|
||||
background: var(--bg-slate-500, #161922);
|
||||
background: var(--Slate-500, #161922);
|
||||
// color: #3f5ecc;
|
||||
}
|
||||
}
|
||||
@@ -56,9 +50,9 @@
|
||||
|
||||
.nav-item-data {
|
||||
flex-grow: 1;
|
||||
max-width: calc(100% - 20px);
|
||||
max-width: calc(100% - 24px);
|
||||
display: flex;
|
||||
margin: 0px 0px 0px 6px;
|
||||
margin: 0px 8px;
|
||||
padding: 2px 8px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -74,7 +68,7 @@
|
||||
|
||||
background: transparent;
|
||||
|
||||
transition: 0.08s all ease;
|
||||
transition: 0.2s all linear;
|
||||
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -106,7 +100,7 @@
|
||||
|
||||
&:hover {
|
||||
.nav-item-label {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
color: var(--Vanilla-100, #fff);
|
||||
}
|
||||
|
||||
.nav-item-pin-icon {
|
||||
@@ -126,12 +120,6 @@
|
||||
.nav-item-active-marker {
|
||||
background: #4e74f8;
|
||||
}
|
||||
|
||||
.nav-item-data {
|
||||
.nav-item-label {
|
||||
color: var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { Tag } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Pin, PinOff } from 'lucide-react';
|
||||
|
||||
import { SidebarItem } from '../sideNav.types';
|
||||
|
||||
import './NavItem.styles.scss';
|
||||
import './NavItem.styles.scss';
|
||||
|
||||
export default function NavItem({
|
||||
@@ -75,25 +74,21 @@ export default function NavItem({
|
||||
)}
|
||||
|
||||
{onTogglePin && !isPinned && (
|
||||
<Tooltip title="Add to shortcuts" placement="right">
|
||||
<Pin
|
||||
size={12}
|
||||
className="nav-item-pin-icon"
|
||||
onClick={handleTogglePinClick}
|
||||
color="var(--Vanilla-400, #c0c1c3)"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Pin
|
||||
size={12}
|
||||
className="nav-item-pin-icon"
|
||||
onClick={handleTogglePinClick}
|
||||
color="var(--Vanilla-400, #c0c1c3)"
|
||||
/>
|
||||
)}
|
||||
|
||||
{onTogglePin && isPinned && (
|
||||
<Tooltip title="Remove from shortcuts" placement="right">
|
||||
<PinOff
|
||||
size={12}
|
||||
className="nav-item-pin-icon"
|
||||
onClick={handleTogglePinClick}
|
||||
color="var(--Vanilla-400, #c0c1c3)"
|
||||
/>
|
||||
</Tooltip>
|
||||
<PinOff
|
||||
size={12}
|
||||
className="nav-item-pin-icon"
|
||||
onClick={handleTogglePinClick}
|
||||
color="var(--Vanilla-400, #c0c1c3)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.sidenav-container {
|
||||
width: 54px;
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -10,60 +10,47 @@
|
||||
}
|
||||
|
||||
.sideNav {
|
||||
flex: 0 0 54px;
|
||||
flex: 0 0 64px;
|
||||
height: 100%;
|
||||
max-width: 54px;
|
||||
min-width: 54px;
|
||||
width: 54px;
|
||||
border-right: 1px solid var(--bg-slate-500, #161922);
|
||||
background: var(--bg-ink-500, #0b0c0e);
|
||||
max-width: 64px;
|
||||
min-width: 64px;
|
||||
width: 64px;
|
||||
border-right: 1px solid var(--Slate-500, #161922);
|
||||
background: var(--Ink-500, #0b0c0e);
|
||||
|
||||
padding-bottom: 48px;
|
||||
transition: all 0.08s ease, background 0s, border 0s;
|
||||
transition: all 0.2s, background 0s, border 0s;
|
||||
|
||||
.brand-container {
|
||||
padding: 8px 15px;
|
||||
padding: 8px 18px;
|
||||
max-width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.brand-company-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
gap: 32px;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
|
||||
.brand-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: 16px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.brand-logo-name {
|
||||
@@ -79,10 +66,6 @@
|
||||
|
||||
.brand-title-section {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
position: relative;
|
||||
|
||||
.license-type {
|
||||
display: flex;
|
||||
@@ -93,7 +76,7 @@
|
||||
|
||||
color: var(--bg-vanilla-100);
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
background: var(--Slate-400, #1d212d);
|
||||
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
@@ -115,11 +98,11 @@
|
||||
gap: 6px;
|
||||
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
background: var(--bg-slate-300, #242834);
|
||||
background: var(--Slate-300, #242834);
|
||||
}
|
||||
|
||||
.version {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
text-align: center;
|
||||
font-variant-numeric: lining-nums tabular-nums slashed-zero;
|
||||
font-feature-settings: 'salt' on;
|
||||
@@ -173,48 +156,24 @@
|
||||
|
||||
.get-started-nav-items {
|
||||
display: flex;
|
||||
margin: 4px 10px 12px 8px;
|
||||
margin: 4px 13px 12px 10px;
|
||||
|
||||
.get-started-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
margin-left: 2px;
|
||||
gap: 8px;
|
||||
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
background: var(--bg-slate-500, #161922);
|
||||
border: 1px solid var(--Slate-400, #1d212d);
|
||||
background: var(--Slate-500, #161922);
|
||||
|
||||
box-shadow: none !important;
|
||||
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
|
||||
svg {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
border-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
|
||||
svg {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,10 +192,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.nav-top-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -272,7 +227,7 @@
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
color: var(--bg-slate-50, #62687c);
|
||||
color: var(--Slate-50, #62687c);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
@@ -286,7 +241,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
padding: 0 17px;
|
||||
padding: 0 20px;
|
||||
|
||||
.nav-section-title-text {
|
||||
display: none;
|
||||
@@ -295,17 +250,11 @@
|
||||
.nav-section-title-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: opacity 0.08s ease, transform 0.08s ease;
|
||||
|
||||
&.reorder {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +268,7 @@
|
||||
}
|
||||
|
||||
.nav-section-subtitle {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
@@ -327,20 +276,20 @@
|
||||
line-height: 14px; /* 150% */
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
padding: 6px 20px;
|
||||
padding: 0 20px;
|
||||
opacity: 0.6;
|
||||
|
||||
display: none;
|
||||
|
||||
transition: all 0.08s ease, background 0s, border 0s;
|
||||
transition-delay: 0.03s;
|
||||
transition: all 0.3s, background 0s, border 0s;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.nav-items-section {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.08s ease;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +302,7 @@
|
||||
.nav-items-section {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.1s ease;
|
||||
transition: all 0.4s ease;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
}
|
||||
@@ -363,34 +312,11 @@
|
||||
.nav-items-section {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: all 0.1s ease;
|
||||
transition: all 0.4s ease;
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.sidebar-collapsed {
|
||||
.nav-title-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-items-section {
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: all 0.08s ease;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-nav-items {
|
||||
&.sidebar-collapsed {
|
||||
.nav-items-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-for-more-container {
|
||||
@@ -400,7 +326,7 @@
|
||||
width: 100%;
|
||||
bottom: 12px;
|
||||
bottom: 8px;
|
||||
margin-left: 43px;
|
||||
margin-left: 50px;
|
||||
|
||||
.scroll-for-more {
|
||||
display: flex;
|
||||
@@ -444,6 +370,8 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
padding-top: 12px;
|
||||
|
||||
.secondary-nav-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -453,10 +381,10 @@
|
||||
overflow-x: hidden;
|
||||
padding: 8px 0;
|
||||
max-width: 100%;
|
||||
width: 54px;
|
||||
width: 64px;
|
||||
// width: 100%; // temp
|
||||
|
||||
transition: all 0.08s ease, background 0s, border 0s;
|
||||
transition: all 0.2s, background 0s, border 0s;
|
||||
|
||||
background: linear-gradient(180deg, rgba(11, 12, 14, 0) 0%, #0b0c0e 27.11%);
|
||||
|
||||
@@ -485,7 +413,7 @@
|
||||
|
||||
&.scroll-available {
|
||||
.nav-bottom-section {
|
||||
border-top: 1px solid var(--bg-slate-500, #161922);
|
||||
border-top: 1px solid var(--Slate-500, #161922);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,53 +424,24 @@
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
flex: 0 0 54px;
|
||||
max-width: 54px;
|
||||
min-width: 54px;
|
||||
width: 54px;
|
||||
flex: 0 0 64px;
|
||||
max-width: 64px;
|
||||
min-width: 64px;
|
||||
width: 64px;
|
||||
|
||||
.nav-wrapper {
|
||||
.nav-top-section {
|
||||
.shortcut-nav-items {
|
||||
.nav-section-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-section-title,
|
||||
.nav-section-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-items-section {
|
||||
display: flex;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.nav-title-section {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.more-nav-items {
|
||||
.nav-section-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-items-section {
|
||||
display: flex;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.nav-title-section {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-bottom-section {
|
||||
.secondary-nav-items {
|
||||
width: 54px;
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -567,7 +466,7 @@
|
||||
border-radius: 12px;
|
||||
background: var(--Robin-500, #4e74f8);
|
||||
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
color: var(--Vanilla-100, #fff);
|
||||
font-variant-numeric: lining-nums tabular-nums slashed-zero;
|
||||
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
|
||||
font-family: Inter;
|
||||
@@ -580,7 +479,7 @@
|
||||
}
|
||||
|
||||
.sidenav-beta-tag {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
color: var(--Vanilla-100, #fff);
|
||||
font-variant-numeric: lining-nums tabular-nums slashed-zero;
|
||||
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
|
||||
font-family: Inter;
|
||||
@@ -595,47 +494,7 @@
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
&:not(.pinned) {
|
||||
.nav-item {
|
||||
.nav-item-data {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-nav-items,
|
||||
.more-nav-items {
|
||||
.nav-section-title {
|
||||
padding: 0 17px;
|
||||
|
||||
.nav-section-title-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dropdown-open {
|
||||
.nav-item {
|
||||
.nav-item-data {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-nav-items,
|
||||
.more-nav-items {
|
||||
.nav-section-title {
|
||||
padding: 0 17px;
|
||||
|
||||
.nav-section-title-icon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.pinned):hover,
|
||||
&.dropdown-open {
|
||||
&:hover {
|
||||
flex: 0 0 240px;
|
||||
max-width: 240px;
|
||||
min-width: 240px;
|
||||
@@ -646,17 +505,8 @@
|
||||
z-index: 10;
|
||||
background: #0b0c0e;
|
||||
|
||||
.brand-container {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
justify-content: flex-start;
|
||||
|
||||
.brand-company-meta {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
justify-content: space-between;
|
||||
|
||||
.brand-title-section {
|
||||
display: flex;
|
||||
@@ -683,11 +533,6 @@
|
||||
.nav-section-title-icon {
|
||||
&.reorder {
|
||||
display: flex;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -729,7 +574,7 @@
|
||||
flex-direction: row;
|
||||
gap: 3px;
|
||||
border-radius: 20px;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
background: var(--Slate-400, #1d212d);
|
||||
|
||||
/* Drop Shadow */
|
||||
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
|
||||
@@ -745,7 +590,7 @@
|
||||
width: 140px;
|
||||
|
||||
.scroll-for-more-label {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
@@ -786,13 +631,6 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
.nav-item-data {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.get-started-nav-items {
|
||||
@@ -826,17 +664,8 @@
|
||||
z-index: 10;
|
||||
background: #0b0c0e;
|
||||
|
||||
.brand-container {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
justify-content: flex-start;
|
||||
|
||||
.brand-company-meta {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
justify-content: space-between;
|
||||
|
||||
.brand-title-section {
|
||||
display: flex;
|
||||
@@ -863,11 +692,6 @@
|
||||
.nav-section-title-icon {
|
||||
&.reorder {
|
||||
display: flex;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -909,7 +733,7 @@
|
||||
flex-direction: row;
|
||||
gap: 3px;
|
||||
border-radius: 20px;
|
||||
background: var(--bg-slate-400, #1d212d);
|
||||
background: var(--Slate-400, #1d212d);
|
||||
|
||||
/* Drop Shadow */
|
||||
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
|
||||
@@ -927,7 +751,7 @@
|
||||
.scroll-for-more-label {
|
||||
display: block;
|
||||
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
@@ -1032,7 +856,7 @@
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
.ant-dropdown-menu-title-content {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
@@ -1040,12 +864,6 @@
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
}
|
||||
|
||||
&:hover:not(.ant-dropdown-menu-item-disabled) {
|
||||
.ant-dropdown-menu-title-content {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1057,7 +875,7 @@
|
||||
gap: 8px;
|
||||
|
||||
.user-settings-dropdown-label-text {
|
||||
color: var(--bg-slate-50, #62687c);
|
||||
color: var(--Slate-50, #62687c);
|
||||
font-family: Inter;
|
||||
font-size: 10px;
|
||||
font-family: Inter;
|
||||
@@ -1069,7 +887,7 @@
|
||||
}
|
||||
|
||||
.user-settings-dropdown-label-email {
|
||||
color: var(--bg-vanilla-400, #c0c1c3);
|
||||
color: var(--Vanilla-400, #c0c1c3);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
@@ -1079,16 +897,12 @@
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-divider {
|
||||
background-color: var(--bg-slate-500, #161922) !important;
|
||||
background-color: var(--Slate-500, #161922) !important;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-dropdown,
|
||||
@@ -1098,27 +912,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-nav-items {
|
||||
.nav-item {
|
||||
position: relative;
|
||||
|
||||
.nav-item-active-marker {
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
width: 8px;
|
||||
height: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item-data {
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.reorder-shortcut-nav-items-modal {
|
||||
width: 384px !important;
|
||||
|
||||
@@ -1235,6 +1028,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--Robin-500, #4e74f8) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
font-family: Inter;
|
||||
@@ -1244,10 +1038,10 @@
|
||||
line-height: 24px;
|
||||
|
||||
&.secondary-btn {
|
||||
background-color: var(--bg-slate-500, #161922) !important;
|
||||
background-color: var(--Slate-500, #161922) !important;
|
||||
border: 1px solid var(--bg-slate-500) !important;
|
||||
|
||||
color: var(--bg-vanilla-400, #c0c1c3) !important;
|
||||
color: var(--Vanilla-400, #c0c1c3) !important;
|
||||
|
||||
/* button/ small */
|
||||
font-family: Inter;
|
||||
@@ -1270,10 +1064,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.help-support-dropdown li.ant-dropdown-menu-item-divider {
|
||||
background-color: var(--bg-slate-500, #161922) !important;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.sideNav {
|
||||
background: var(--bg-vanilla-100);
|
||||
@@ -1305,32 +1095,8 @@
|
||||
.get-started-nav-items {
|
||||
.get-started-btn {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--bg-slate-50, #62687c);
|
||||
|
||||
svg {
|
||||
color: var(--bg-slate-50, #62687c);
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
color: var(--bg-ink-400, #62687c);
|
||||
}
|
||||
|
||||
// Hover state (light mode)
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--bg-vanilla-300);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
color: var(--bg-slate-500, #161922);
|
||||
|
||||
svg {
|
||||
color: var(--bg-slate-500, #161922);
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
color: var(--bg-slate-500, #161922);
|
||||
}
|
||||
}
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,25 +1108,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.brand-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
.nav-top-section {
|
||||
.shortcut-nav-items {
|
||||
.nav-section-title {
|
||||
.nav-section-title-icon {
|
||||
&.reorder {
|
||||
&:hover {
|
||||
color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-nav-items {
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
@@ -1375,43 +1123,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.pinned {
|
||||
.nav-wrapper {
|
||||
.nav-top-section {
|
||||
.shortcut-nav-items {
|
||||
.nav-section-title {
|
||||
.nav-section-title-icon {
|
||||
&.reorder {
|
||||
&:hover {
|
||||
color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.pinned):hover,
|
||||
&.dropdown-open {
|
||||
&:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.nav-wrapper {
|
||||
.nav-top-section {
|
||||
.shortcut-nav-items {
|
||||
.nav-section-title {
|
||||
.nav-section-title-icon {
|
||||
&.reorder {
|
||||
&:hover {
|
||||
color: var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,12 +1134,6 @@
|
||||
.ant-dropdown-menu-title-content {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&:hover:not(.ant-dropdown-menu-item-disabled) {
|
||||
.ant-dropdown-menu-title-content {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1503,10 +1210,6 @@
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.help-support-dropdown li.ant-dropdown-menu-item-divider {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.version-tooltip-overlay {
|
||||
@@ -1519,7 +1222,7 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
color: var(--Vanilla-100, #fff);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
@@ -1534,7 +1237,7 @@
|
||||
gap: 4px;
|
||||
|
||||
.version-update-notification-tooltip-title {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
color: var(--Vanilla-100, #fff);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
@@ -1544,7 +1247,7 @@
|
||||
}
|
||||
|
||||
.version-update-notification-tooltip-content {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
color: var(--Vanilla-100, #fff);
|
||||
font-family: Inter;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
|
||||
@@ -157,27 +157,18 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
DefaultHelpSupportDropdownMenuItems,
|
||||
);
|
||||
|
||||
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
|
||||
|
||||
const [tempPinnedMenuItems, setTempPinnedMenuItems] = useState<SidebarItem[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const [secondaryMenuItems, setSecondaryMenuItems] = useState<SidebarItem[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const [hasScroll, setHasScroll] = useState(false);
|
||||
const navTopSectionRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [pinnedMenuItems, setPinnedMenuItems] = useState<SidebarItem[]>([]);
|
||||
const [secondaryMenuItems, setSecondaryMenuItems] = useState<SidebarItem[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
const checkScroll = useCallback((): void => {
|
||||
if (navTopSectionRef.current) {
|
||||
@@ -226,68 +217,63 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||
const isEditor = user.role === USER_ROLES.EDITOR;
|
||||
|
||||
// Compute initial pinned items and secondary menu items synchronously to avoid flash
|
||||
const computedPinnedMenuItems = useMemo(() => {
|
||||
const navShortcutsPreference = userPreferences?.find(
|
||||
useEffect(() => {
|
||||
const navShortcuts = (userPreferences?.find(
|
||||
(preference) => preference.name === USER_PREFERENCES.NAV_SHORTCUTS,
|
||||
);
|
||||
const navShortcuts = (navShortcutsPreference?.value as unknown) as
|
||||
| string[]
|
||||
| undefined;
|
||||
)?.value as unknown) as string[];
|
||||
|
||||
// If userPreferences not loaded yet, return empty to avoid showing defaults before preferences load
|
||||
if (userPreferences === null) {
|
||||
return [];
|
||||
}
|
||||
const shouldShowIntegrations =
|
||||
(isCloudUser || isEnterpriseSelfHostedUser) && (isAdmin || isEditor);
|
||||
|
||||
// If preference exists with non-empty array, use stored shortcuts
|
||||
if (isArray(navShortcuts) && navShortcuts.length > 0) {
|
||||
return navShortcuts
|
||||
if (navShortcuts && isArray(navShortcuts) && navShortcuts.length > 0) {
|
||||
// nav shortcuts is array of strings
|
||||
const pinnedItems = navShortcuts
|
||||
.map((shortcut) =>
|
||||
defaultMoreMenuItems.find((item) => item.itemKey === shortcut),
|
||||
)
|
||||
.filter((item): item is SidebarItem => item !== undefined);
|
||||
|
||||
// Set pinned items in the order they were stored
|
||||
setPinnedMenuItems(pinnedItems);
|
||||
|
||||
setSecondaryMenuItems(
|
||||
defaultMoreMenuItems.map((item) => ({
|
||||
...item,
|
||||
isPinned: pinnedItems.some((pinned) => pinned.itemKey === item.itemKey),
|
||||
isEnabled:
|
||||
item.key === ROUTES.INTEGRATIONS
|
||||
? shouldShowIntegrations
|
||||
: item.isEnabled,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
// Set default pinned items
|
||||
const defaultPinnedItems = defaultMoreMenuItems.filter(
|
||||
(item) => item.isPinned,
|
||||
);
|
||||
setPinnedMenuItems(defaultPinnedItems);
|
||||
|
||||
setSecondaryMenuItems(
|
||||
defaultMoreMenuItems.map((item) => ({
|
||||
...item,
|
||||
isPinned: defaultPinnedItems.some(
|
||||
(pinned) => pinned.itemKey === item.itemKey,
|
||||
),
|
||||
isEnabled:
|
||||
item.key === ROUTES.INTEGRATIONS
|
||||
? shouldShowIntegrations
|
||||
: item.isEnabled,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
// No preference, or empty array → use defaults
|
||||
return defaultMoreMenuItems.filter((item) => item.isPinned);
|
||||
}, [userPreferences]);
|
||||
|
||||
const computedSecondaryMenuItems = useMemo(() => {
|
||||
const shouldShowIntegrationsValue =
|
||||
(isCloudUser || isEnterpriseSelfHostedUser) && (isAdmin || isEditor);
|
||||
|
||||
return defaultMoreMenuItems.map((item) => ({
|
||||
...item,
|
||||
isPinned: computedPinnedMenuItems.some(
|
||||
(pinned) => pinned.itemKey === item.itemKey,
|
||||
),
|
||||
isEnabled:
|
||||
item.key === ROUTES.INTEGRATIONS
|
||||
? shouldShowIntegrationsValue
|
||||
: item.isEnabled,
|
||||
}));
|
||||
}, [
|
||||
computedPinnedMenuItems,
|
||||
userPreferences,
|
||||
isCloudUser,
|
||||
isEnterpriseSelfHostedUser,
|
||||
isAdmin,
|
||||
isEditor,
|
||||
]);
|
||||
|
||||
// Track if we've done the initial sync (to avoid overwriting user actions during session)
|
||||
const hasInitializedRef = useRef(false);
|
||||
|
||||
// Sync state only on initial load when userPreferences first becomes available
|
||||
useEffect(() => {
|
||||
// Only sync once: when userPreferences loads for the first time
|
||||
if (!hasInitializedRef.current && userPreferences !== null) {
|
||||
setPinnedMenuItems(computedPinnedMenuItems);
|
||||
setSecondaryMenuItems(computedSecondaryMenuItems);
|
||||
hasInitializedRef.current = true;
|
||||
}
|
||||
}, [computedPinnedMenuItems, computedSecondaryMenuItems, userPreferences]);
|
||||
|
||||
const isOnboardingV3Enabled = featureFlags?.find(
|
||||
(flag) => flag.name === FeatureKeys.ONBOARDING_V3,
|
||||
)?.active;
|
||||
@@ -341,17 +327,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
.map((item) => item.itemKey)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
// Update context immediately (optimistically) so computed values reflect the change
|
||||
updateUserPreferenceInContext({
|
||||
name: USER_PREFERENCES.NAV_SHORTCUTS,
|
||||
description: USER_PREFERENCES.NAV_SHORTCUTS,
|
||||
valueType: 'array',
|
||||
defaultValue: false,
|
||||
allowedValues: [],
|
||||
allowedScopes: ['user'],
|
||||
value: navShortcuts,
|
||||
});
|
||||
|
||||
updateUserPreferenceMutation(
|
||||
{
|
||||
name: USER_PREFERENCES.NAV_SHORTCUTS,
|
||||
@@ -360,7 +335,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
if (response.data) {
|
||||
// Update context again on success to ensure consistency
|
||||
updateUserPreferenceInContext({
|
||||
name: USER_PREFERENCES.NAV_SHORTCUTS,
|
||||
description: USER_PREFERENCES.NAV_SHORTCUTS,
|
||||
@@ -394,13 +368,13 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
if (isCurrentlyPinned) {
|
||||
return prevItems.filter((i) => i.key !== item.key);
|
||||
}
|
||||
return [...prevItems, item];
|
||||
return [item, ...prevItems];
|
||||
});
|
||||
|
||||
// Get the updated pinned menu items for preference update
|
||||
const updatedPinnedItems = pinnedMenuItems.some((i) => i.key === item.key)
|
||||
? pinnedMenuItems.filter((i) => i.key !== item.key)
|
||||
: [...pinnedMenuItems, item];
|
||||
: [item, ...pinnedMenuItems];
|
||||
|
||||
// Update user preference with the ordered list of item keys
|
||||
updateNavShortcutsPreference(updatedPinnedItems);
|
||||
@@ -481,10 +455,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const isSettingsPage = useMemo(() => pathname.startsWith(ROUTES.SETTINGS), [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const userSettingsDropdownMenuItems: MenuProps['items'] = useMemo(
|
||||
() =>
|
||||
[
|
||||
@@ -624,7 +594,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
label: "WHAT'S NEW",
|
||||
label: "WHAT's NEW",
|
||||
},
|
||||
...dropdownItems,
|
||||
{
|
||||
@@ -780,15 +750,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
[secondaryMenuItems],
|
||||
);
|
||||
|
||||
// Get active "More" items that should be visible in collapsed state
|
||||
const activeMoreMenuItems = useMemo(
|
||||
() => moreMenuItems.filter((item) => activeMenuKey === item.key),
|
||||
[moreMenuItems, activeMenuKey],
|
||||
);
|
||||
|
||||
// Check if sidebar is collapsed (not pinned, not hovered, and no dropdown open)
|
||||
const isCollapsed = !isPinned && !isHovered && !isDropdownOpen;
|
||||
|
||||
const renderNavItems = (
|
||||
items: SidebarItem[],
|
||||
allowPin?: boolean,
|
||||
@@ -940,15 +901,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className={cx('sidenav-container', isPinned && 'pinned')}>
|
||||
<div
|
||||
className={cx(
|
||||
'sideNav',
|
||||
isPinned && 'pinned',
|
||||
isDropdownOpen && 'dropdown-open',
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className={cx('sideNav', isPinned && 'pinned')}>
|
||||
<div className="brand-container">
|
||||
<div className="brand">
|
||||
<div className="brand-company-meta">
|
||||
@@ -1046,43 +999,35 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
{renderNavItems(primaryMenuItems)}
|
||||
</div>
|
||||
|
||||
{(pinnedMenuItems.length > 0 || !isCollapsed) && (
|
||||
<div
|
||||
className={cx('shortcut-nav-items', isCollapsed && 'sidebar-collapsed')}
|
||||
>
|
||||
{!isCollapsed && (
|
||||
<div className="nav-title-section">
|
||||
<div className="nav-section-title">
|
||||
<div className="nav-section-title-icon">
|
||||
<MousePointerClick size={16} />
|
||||
</div>
|
||||
<div className="shortcut-nav-items">
|
||||
<div className="nav-title-section">
|
||||
<div className="nav-section-title">
|
||||
<div className="nav-section-title-icon">
|
||||
<MousePointerClick size={16} />
|
||||
</div>
|
||||
|
||||
<div className="nav-section-title-text">SHORTCUTS</div>
|
||||
<div className="nav-section-title-text">SHORTCUTS</div>
|
||||
|
||||
{pinnedMenuItems.length > 1 && (
|
||||
<Tooltip title="Manage shortcuts" placement="right">
|
||||
<div
|
||||
className="nav-section-title-icon reorder"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts clicked', {});
|
||||
setIsReorderShortcutNavItemsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Logs size={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{pinnedMenuItems.length > 1 && (
|
||||
<div
|
||||
className="nav-section-title-icon reorder"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts clicked', {});
|
||||
setIsReorderShortcutNavItemsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Logs size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pinnedMenuItems.length === 0 && (
|
||||
<div className="nav-section-subtitle">
|
||||
You have not added any shortcuts yet.
|
||||
</div>
|
||||
)}
|
||||
{pinnedMenuItems.length === 0 && (
|
||||
<div className="nav-section-subtitle">
|
||||
You have not added any shortcuts yet.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(pinnedMenuItems.length > 0 || isCollapsed) && (
|
||||
{pinnedMenuItems.length > 0 && (
|
||||
<div className="nav-items-section">
|
||||
{renderNavItems(
|
||||
pinnedMenuItems.filter((item) => item.isEnabled),
|
||||
@@ -1091,60 +1036,46 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{moreMenuItems.length > 0 && (
|
||||
<div
|
||||
className={cx(
|
||||
'more-nav-items',
|
||||
isMoreMenuCollapsed ? 'collapsed' : 'expanded',
|
||||
isCollapsed && 'sidebar-collapsed',
|
||||
)}
|
||||
>
|
||||
{!isCollapsed && (
|
||||
<div className="nav-title-section">
|
||||
<div
|
||||
className="nav-section-title"
|
||||
onClick={(): void => {
|
||||
// Only allow toggling when sidebar is open (pinned, hovered, or dropdown open)
|
||||
if (isCollapsed) {
|
||||
return;
|
||||
}
|
||||
const newCollapsedState = !isMoreMenuCollapsed;
|
||||
logEvent('Sidebar V2: More menu clicked', {
|
||||
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
|
||||
});
|
||||
setIsMoreMenuCollapsed(newCollapsedState);
|
||||
}}
|
||||
>
|
||||
<div className="nav-section-title-icon">
|
||||
<Ellipsis size={16} />
|
||||
</div>
|
||||
<div className="nav-title-section">
|
||||
<div
|
||||
className="nav-section-title"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: More menu clicked', {
|
||||
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
|
||||
});
|
||||
setIsMoreMenuCollapsed(!isMoreMenuCollapsed);
|
||||
}}
|
||||
>
|
||||
<div className="nav-section-title-icon">
|
||||
<Ellipsis size={16} />
|
||||
</div>
|
||||
|
||||
<div className="nav-section-title-text">MORE</div>
|
||||
<div className="nav-section-title-text">MORE</div>
|
||||
|
||||
<div className="collapse-expand-section-icon">
|
||||
{isMoreMenuCollapsed ? (
|
||||
<ChevronDown size={16} />
|
||||
) : (
|
||||
<ChevronUp size={16} />
|
||||
)}
|
||||
</div>
|
||||
<div className="collapse-expand-section-icon">
|
||||
{isMoreMenuCollapsed ? (
|
||||
<ChevronDown size={16} />
|
||||
) : (
|
||||
<ChevronUp size={16} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="nav-items-section">
|
||||
{/* Show all items when expanded, only active items when collapsed */}
|
||||
{isCollapsed
|
||||
? renderNavItems(
|
||||
activeMoreMenuItems.filter((item) => item.isEnabled),
|
||||
true,
|
||||
)
|
||||
: renderNavItems(
|
||||
moreMenuItems.filter((item) => item.isEnabled),
|
||||
true,
|
||||
)}
|
||||
{renderNavItems(
|
||||
moreMenuItems.filter((item) => item.isEnabled),
|
||||
true,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1171,7 +1102,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
placement="topLeft"
|
||||
overlayClassName="nav-dropdown-overlay help-support-dropdown"
|
||||
trigger={['click']}
|
||||
onOpenChange={(open): void => setIsDropdownOpen(open)}
|
||||
>
|
||||
<div className="nav-item">
|
||||
<div className="nav-item-data" data-testid="help-support-nav-item">
|
||||
@@ -1192,10 +1122,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
placement="topLeft"
|
||||
overlayClassName="nav-dropdown-overlay settings-dropdown"
|
||||
trigger={['click']}
|
||||
onOpenChange={(open): void => setIsDropdownOpen(open)}
|
||||
>
|
||||
<div className={cx('nav-item', isSettingsPage && 'active')}>
|
||||
<div className="nav-item-active-marker" />
|
||||
<div className="nav-item">
|
||||
<div className="nav-item-data" data-testid="settings-nav-item">
|
||||
<div className="nav-item-icon">{userSettingsMenuItem.icon}</div>
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
|
||||
import useGetResolvedText from '../useGetResolvedText';
|
||||
|
||||
// Create a mock function that we can modify per test
|
||||
let mockDashboardVariables: IDashboardVariables = {};
|
||||
|
||||
// Mock the useDashboardVariables hook
|
||||
jest.mock('hooks/dashboard/useDashboardVariables', () => ({
|
||||
useDashboardVariables: jest.fn(() => ({
|
||||
dashboardVariables: mockDashboardVariables,
|
||||
})),
|
||||
// Mock the useDashboard hook
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: function useDashboardMock(): any {
|
||||
return {
|
||||
selectedDashboard: null,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useGetResolvedText', () => {
|
||||
@@ -22,35 +20,13 @@ describe('useGetResolvedText', () => {
|
||||
const TRUNCATED_SERVICE = 'test, app +2';
|
||||
const TEXT_TEMPLATE = 'Logs count in $service.name in $severity';
|
||||
|
||||
const renderHookWithProps = (
|
||||
props: {
|
||||
text: string | React.ReactNode;
|
||||
maxLength?: number;
|
||||
matcher?: string;
|
||||
},
|
||||
variables?: Record<string, string | number | boolean>,
|
||||
): any => {
|
||||
if (variables) {
|
||||
mockDashboardVariables = Object.entries(
|
||||
variables,
|
||||
).reduce<IDashboardVariables>((acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
id: key,
|
||||
name: key,
|
||||
description: '',
|
||||
type: 'CUSTOM' as const,
|
||||
sort: 'DISABLED' as const,
|
||||
multiSelect: false,
|
||||
showALLOption: false,
|
||||
selectedValue: value,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
mockDashboardVariables = {};
|
||||
}
|
||||
return renderHook(() => useGetResolvedText(props));
|
||||
};
|
||||
const renderHookWithProps = (props: {
|
||||
text: string | React.ReactNode;
|
||||
variables?: Record<string, string | number | boolean>;
|
||||
dashboardVariables?: Record<string, any>;
|
||||
maxLength?: number;
|
||||
matcher?: string;
|
||||
}): any => renderHook(() => useGetResolvedText(props));
|
||||
|
||||
it('should resolve variables with truncated and full text', () => {
|
||||
const text = TEXT_TEMPLATE;
|
||||
@@ -59,7 +35,7 @@ describe('useGetResolvedText', () => {
|
||||
severity: SEVERITY_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
`Logs count in ${TRUNCATED_SERVICE} in DEBUG, INFO`,
|
||||
@@ -74,7 +50,7 @@ describe('useGetResolvedText', () => {
|
||||
severity: SEVERITY_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text, maxLength: 20 }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables, maxLength: 20 });
|
||||
|
||||
expect(result.current.truncatedText).toBe('Logs count in test, a...');
|
||||
expect(result.current.fullText).toBe(EXPECTED_FULL_TEXT);
|
||||
@@ -86,7 +62,7 @@ describe('useGetResolvedText', () => {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs count in test, app +2 and test, app +2',
|
||||
@@ -104,7 +80,7 @@ describe('useGetResolvedText', () => {
|
||||
'$dyn-service.name': 'dyn-1, dyn-2',
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs in test, app +2, test, app +2, test, app +2 - dyn-1, dyn-2',
|
||||
@@ -121,7 +97,7 @@ describe('useGetResolvedText', () => {
|
||||
severity: SEVERITY_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text, matcher: '#' }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables, matcher: '#' });
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs count in test, app +2 in DEBUG, INFO',
|
||||
@@ -136,7 +112,7 @@ describe('useGetResolvedText', () => {
|
||||
active: true,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.fullText).toBe('Count: 42, Active: true');
|
||||
expect(result.current.truncatedText).toBe('Count: 42, Active: true');
|
||||
@@ -148,7 +124,7 @@ describe('useGetResolvedText', () => {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.truncatedText).toBe(
|
||||
'Logs count in test, app +2 in $unknown',
|
||||
@@ -164,12 +140,10 @@ describe('useGetResolvedText', () => {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps(
|
||||
{
|
||||
text: reactNodeText,
|
||||
},
|
||||
const { result } = renderHookWithProps({
|
||||
text: reactNodeText,
|
||||
variables,
|
||||
);
|
||||
});
|
||||
|
||||
// Should return the ReactNode unchanged
|
||||
expect(result.current.fullText).toBe(reactNodeText);
|
||||
@@ -182,12 +156,10 @@ describe('useGetResolvedText', () => {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps(
|
||||
{
|
||||
text,
|
||||
},
|
||||
const { result } = renderHookWithProps({
|
||||
text,
|
||||
variables,
|
||||
);
|
||||
});
|
||||
|
||||
// Should return the number unchanged
|
||||
expect(result.current.fullText).toBe(text);
|
||||
@@ -200,12 +172,10 @@ describe('useGetResolvedText', () => {
|
||||
'service.name': SERVICE_VAR,
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps(
|
||||
{
|
||||
text,
|
||||
},
|
||||
const { result } = renderHookWithProps({
|
||||
text,
|
||||
variables,
|
||||
);
|
||||
});
|
||||
|
||||
// Should return the boolean unchanged
|
||||
expect(result.current.fullText).toBe(text);
|
||||
@@ -219,7 +189,7 @@ describe('useGetResolvedText', () => {
|
||||
'config.database.host': 'localhost:5432',
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.fullText).toBe('API: /users Config: localhost:5432');
|
||||
expect(result.current.truncatedText).toBe(
|
||||
@@ -234,7 +204,7 @@ describe('useGetResolvedText', () => {
|
||||
'error.type': 'timeout',
|
||||
};
|
||||
|
||||
const { result } = renderHookWithProps({ text }, variables);
|
||||
const { result } = renderHookWithProps({ text, variables });
|
||||
|
||||
expect(result.current.fullText).toBe('Status: web-api, Error: timeout;');
|
||||
expect(result.current.truncatedText).toBe('Status: web-api, Error: timeout;');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@@ -38,17 +38,20 @@ interface ResolvedTextUtilsResult {
|
||||
|
||||
function useContextVariables({
|
||||
maxValues = 2,
|
||||
// ! To be noted: This customVariables is not Dashboard Custom Variables
|
||||
customVariables,
|
||||
}: UseContextVariablesProps): UseContextVariablesResult {
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
// Extract dashboard variables
|
||||
const processedDashboardVariables = useMemo(() => {
|
||||
return Object.entries(dashboardVariables)
|
||||
const dashboardVariables = useMemo(() => {
|
||||
if (!selectedDashboard?.data?.variables) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(selectedDashboard.data.variables)
|
||||
.filter(([, value]) => value.name)
|
||||
.map(([, value]) => {
|
||||
let processedValue: string | number | boolean;
|
||||
@@ -71,7 +74,7 @@ function useContextVariables({
|
||||
originalValue: value.selectedValue,
|
||||
};
|
||||
});
|
||||
}, [dashboardVariables]);
|
||||
}, [selectedDashboard]);
|
||||
|
||||
// Extract global variables
|
||||
const globalVariables = useMemo(
|
||||
@@ -108,12 +111,8 @@ function useContextVariables({
|
||||
|
||||
// Combine all variables
|
||||
const allVariables = useMemo(
|
||||
() => [
|
||||
...processedDashboardVariables,
|
||||
...globalVariables,
|
||||
...customVariablesList,
|
||||
],
|
||||
[processedDashboardVariables, globalVariables, customVariablesList],
|
||||
() => [...dashboardVariables, ...globalVariables, ...customVariablesList],
|
||||
[dashboardVariables, globalVariables, customVariablesList],
|
||||
);
|
||||
|
||||
// Create processed variables with truncation logic
|
||||
|
||||
@@ -1,40 +1,19 @@
|
||||
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
||||
import { dashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
import {
|
||||
IDashboardVariablesStoreState,
|
||||
IUseDashboardVariablesReturn,
|
||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
dashboardVariablesStore,
|
||||
IDashboardVariables,
|
||||
} from '../../providers/Dashboard/store/dashboardVariablesStore';
|
||||
|
||||
/**
|
||||
* Generic selector hook for dashboard variables store
|
||||
* Allows granular subscriptions to any part of the store state
|
||||
*
|
||||
* @example
|
||||
* ! Select top-level field
|
||||
* const variables = useDashboardVariablesSelector(s => s.variables);
|
||||
*
|
||||
* ! Select specific variable
|
||||
* const fooVar = useDashboardVariablesSelector(s => s.variables['foo']);
|
||||
*
|
||||
* ! Select derived value
|
||||
* const hasVariables = useDashboardVariablesSelector(s => Object.keys(s.variables).length > 0);
|
||||
*/
|
||||
export const useDashboardVariablesSelector = <T>(
|
||||
selector: (state: IDashboardVariablesStoreState) => T,
|
||||
): T => {
|
||||
const selectorRef = useRef(selector);
|
||||
selectorRef.current = selector;
|
||||
|
||||
const getSnapshot = useCallback(
|
||||
() => selectorRef.current(dashboardVariablesStore.getSnapshot()),
|
||||
[],
|
||||
export const useDashboardVariables = (): {
|
||||
dashboardVariables: IDashboardVariables;
|
||||
} => {
|
||||
const dashboardVariables = useSyncExternalStore(
|
||||
dashboardVariablesStore.subscribe,
|
||||
dashboardVariablesStore.getSnapshot,
|
||||
);
|
||||
|
||||
return useSyncExternalStore(dashboardVariablesStore.subscribe, getSnapshot);
|
||||
};
|
||||
|
||||
export const useDashboardVariables = (): IUseDashboardVariablesReturn => {
|
||||
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
|
||||
|
||||
return { dashboardVariables };
|
||||
return {
|
||||
dashboardVariables,
|
||||
};
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user