Compare commits

..

2 Commits

Author SHA1 Message Date
Piyush Singariya
892cd04475 fix: query args fix 2026-01-30 15:27:34 +05:30
Piyush Singariya
a2aa7d7342 fix: replace promoted paths table 2026-01-30 15:15:04 +05:30
210 changed files with 1435 additions and 11456 deletions

3
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -42,11 +42,10 @@ jobs:
- callbackauthn
- cloudintegrations
- dashboard
- logspipelines
- preference
- querier
- role
- ttl
- preference
- logspipelines
- alerts
sqlstore-provider:
- postgres

View File

@@ -11,8 +11,5 @@
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
}
}

View File

@@ -66,17 +66,6 @@ Read [more](https://signoz.io/metrics-and-dashboards/).
![metrics-n-dashboards-cover](https://github.com/user-attachments/assets/a536fd71-1d2c-4681-aa7e-516d754c47a5)
### LLM Observability
Monitor and debug your LLM applications with comprehensive observability. Track LLM calls, analyze token usage, monitor performance, and gain insights into your AI application's behavior in production.
SigNoz LLM observability helps you understand how your language models are performing, identify issues with prompts and responses, track token usage and costs, and optimize your AI applications for better performance and reliability.
[Get started with LLM Observability →](https://signoz.io/docs/llm-observability/)
![llm-observability-cover](https://github.com/user-attachments/assets/a6cc0ca3-59df-48f9-9c16-7c843fccff96)
### Alerts
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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:
@@ -2665,7 +2643,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2720,7 +2697,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2776,7 +2752,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2943,7 +2918,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -3811,9 +3785,6 @@ components:
type: string
alertName:
type: string
required:
- alertName
- alertId
type: object
MetricsexplorertypesMetricAlertsResponse:
properties:
@@ -3822,8 +3793,6 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesMetricAlert'
nullable: true
type: array
required:
- alerts
type: object
MetricsexplorertypesMetricAttribute:
properties:
@@ -3837,10 +3806,6 @@ components:
type: string
nullable: true
type: array
required:
- key
- values
- valueCount
type: object
MetricsexplorertypesMetricAttributesRequest:
properties:
@@ -3852,8 +3817,6 @@ components:
start:
nullable: true
type: integer
required:
- metricName
type: object
MetricsexplorertypesMetricAttributesResponse:
properties:
@@ -3865,9 +3828,6 @@ components:
totalKeys:
format: int64
type: integer
required:
- attributes
- totalKeys
type: object
MetricsexplorertypesMetricDashboard:
properties:
@@ -3879,11 +3839,6 @@ components:
type: string
widgetName:
type: string
required:
- dashboardName
- dashboardId
- widgetId
- widgetName
type: object
MetricsexplorertypesMetricDashboardsResponse:
properties:
@@ -3892,8 +3847,6 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
nullable: true
type: array
required:
- dashboards
type: object
MetricsexplorertypesMetricHighlightsResponse:
properties:
@@ -3909,11 +3862,6 @@ components:
totalTimeSeries:
minimum: 0
type: integer
required:
- dataPoints
- lastReceived
- totalTimeSeries
- activeTimeSeries
type: object
MetricsexplorertypesMetricMetadata:
properties:
@@ -3922,27 +3870,11 @@ components:
isMonotonic:
type: boolean
temporality:
enum:
- delta
- cumulative
- unspecified
type: string
type:
enum:
- gauge
- sum
- histogram
- summary
- exponentialhistogram
type: string
unit:
type: string
required:
- description
- type
- unit
- temporality
- isMonotonic
type: object
MetricsexplorertypesStat:
properties:
@@ -3957,22 +3889,9 @@ components:
minimum: 0
type: integer
type:
enum:
- gauge
- sum
- histogram
- summary
- exponentialhistogram
type: string
unit:
type: string
required:
- metricName
- description
- type
- unit
- timeseries
- samples
type: object
MetricsexplorertypesStatsRequest:
properties:
@@ -3990,10 +3909,6 @@ components:
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
MetricsexplorertypesStatsResponse:
properties:
@@ -4005,9 +3920,6 @@ components:
total:
minimum: 0
type: integer
required:
- metrics
- total
type: object
MetricsexplorertypesTreemapEntry:
properties:
@@ -4019,10 +3931,6 @@ components:
totalValue:
minimum: 0
type: integer
required:
- metricName
- percentage
- totalValue
type: object
MetricsexplorertypesTreemapRequest:
properties:
@@ -4034,18 +3942,10 @@ components:
limit:
type: integer
mode:
enum:
- timeseries
- samples
type: string
start:
format: int64
type: integer
required:
- start
- end
- limit
- mode
type: object
MetricsexplorertypesTreemapResponse:
properties:
@@ -4059,9 +3959,6 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesTreemapEntry'
nullable: true
type: array
required:
- timeseries
- samples
type: object
MetricsexplorertypesUpdateMetricMetadataRequest:
properties:
@@ -4072,28 +3969,11 @@ components:
metricName:
type: string
temporality:
enum:
- delta
- cumulative
- unspecified
type: string
type:
enum:
- gauge
- sum
- histogram
- summary
- exponentialhistogram
type: string
unit:
type: string
required:
- metricName
- type
- description
- unit
- temporality
- isMonotonic
type: object
PreferencetypesPreference:
properties:

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
})
}
}

View File

@@ -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",
@@ -105,7 +105,6 @@
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"less": "^4.1.2",

View File

@@ -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 = (

View File

@@ -28,10 +28,8 @@ import type {
GatewaytypesPostableIngestionKeyLimitDTO,
GatewaytypesUpdatableIngestionKeyLimitDTO,
GetIngestionKeys200,
GetIngestionKeysParams,
RenderErrorResponseDTO,
SearchIngestionKeys200,
SearchIngestionKeysParams,
UpdateIngestionKeyLimitPathParameters,
UpdateIngestionKeyPathParameters,
} from '../sigNoz.schemas';
@@ -44,44 +42,35 @@ type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
* This endpoint returns the ingestion keys for a workspace
* @summary Get ingestion keys for workspace
*/
export const getIngestionKeys = (
params?: GetIngestionKeysParams,
signal?: AbortSignal,
) => {
export const getIngestionKeys = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetIngestionKeys200>({
url: `/api/v2/gateway/ingestion_keys`,
method: 'GET',
params,
signal,
});
};
export const getGetIngestionKeysQueryKey = (
params?: GetIngestionKeysParams,
) => {
return ['getIngestionKeys', ...(params ? [params] : [])] as const;
export const getGetIngestionKeysQueryKey = () => {
return ['getIngestionKeys'] as const;
};
export const getGetIngestionKeysQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionKeys>>,
TError = RenderErrorResponseDTO
>(
params?: GetIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
},
) => {
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetIngestionKeysQueryKey(params);
const queryKey = queryOptions?.queryKey ?? getGetIngestionKeysQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getIngestionKeys>>> = ({
signal,
}) => getIngestionKeys(params, signal);
}) => getIngestionKeys(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
@@ -102,17 +91,14 @@ export type GetIngestionKeysQueryError = RenderErrorResponseDTO;
export function useGetIngestionKeys<
TData = Awaited<ReturnType<typeof getIngestionKeys>>,
TError = RenderErrorResponseDTO
>(
params?: GetIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeysQueryOptions(params, options);
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeysQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -128,11 +114,10 @@ export function useGetIngestionKeys<
*/
export const invalidateGetIngestionKeys = async (
queryClient: QueryClient,
params?: GetIngestionKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionKeysQueryKey(params) },
{ queryKey: getGetIngestionKeysQueryKey() },
options,
);
@@ -677,45 +662,35 @@ export const useUpdateIngestionKeyLimit = <
* This endpoint returns the ingestion keys for a workspace
* @summary Search ingestion keys for workspace
*/
export const searchIngestionKeys = (
params?: SearchIngestionKeysParams,
signal?: AbortSignal,
) => {
export const searchIngestionKeys = (signal?: AbortSignal) => {
return GeneratedAPIInstance<SearchIngestionKeys200>({
url: `/api/v2/gateway/ingestion_keys/search`,
method: 'GET',
params,
signal,
});
};
export const getSearchIngestionKeysQueryKey = (
params?: SearchIngestionKeysParams,
) => {
return ['searchIngestionKeys', ...(params ? [params] : [])] as const;
export const getSearchIngestionKeysQueryKey = () => {
return ['searchIngestionKeys'] as const;
};
export const getSearchIngestionKeysQueryOptions = <
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
TError = RenderErrorResponseDTO
>(
params?: SearchIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
},
) => {
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getSearchIngestionKeysQueryKey(params);
const queryKey = queryOptions?.queryKey ?? getSearchIngestionKeysQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof searchIngestionKeys>>
> = ({ signal }) => searchIngestionKeys(params, signal);
> = ({ signal }) => searchIngestionKeys(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
@@ -736,17 +711,14 @@ export type SearchIngestionKeysQueryError = RenderErrorResponseDTO;
export function useSearchIngestionKeys<
TData = Awaited<ReturnType<typeof searchIngestionKeys>>,
TError = RenderErrorResponseDTO
>(
params?: SearchIngestionKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getSearchIngestionKeysQueryOptions(params, options);
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof searchIngestionKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getSearchIngestionKeysQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -762,11 +734,10 @@ export function useSearchIngestionKeys<
*/
export const invalidateSearchIngestionKeys = async (
queryClient: QueryClient,
params?: SearchIngestionKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getSearchIngestionKeysQueryKey(params) },
{ queryKey: getSearchIngestionKeysQueryKey() },
options,
);

View File

@@ -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) => {

View File

@@ -47,7 +47,7 @@ type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
* @summary Get metric alerts
*/
export const getMetricAlerts = (
params: GetMetricAlertsParams,
params?: GetMetricAlertsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAlerts200>({
@@ -66,7 +66,7 @@ export const getGetMetricAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricAlertsParams,
params?: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -103,7 +103,7 @@ export function useGetMetricAlerts<
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricAlertsParams,
params?: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -128,7 +128,7 @@ export function useGetMetricAlerts<
*/
export const invalidateGetMetricAlerts = async (
queryClient: QueryClient,
params: GetMetricAlertsParams,
params?: GetMetricAlertsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
@@ -144,7 +144,7 @@ export const invalidateGetMetricAlerts = async (
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
params: GetMetricDashboardsParams,
params?: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
@@ -165,7 +165,7 @@ export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricDashboardsParams,
params?: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -203,7 +203,7 @@ export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricDashboardsParams,
params?: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -228,7 +228,7 @@ export function useGetMetricDashboards<
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
params: GetMetricDashboardsParams,
params?: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
@@ -244,7 +244,7 @@ export const invalidateGetMetricDashboards = async (
* @summary Get metric highlights
*/
export const getMetricHighlights = (
params: GetMetricHighlightsParams,
params?: GetMetricHighlightsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricHighlights200>({
@@ -265,7 +265,7 @@ export const getGetMetricHighlightsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricHighlightsParams,
params?: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -303,7 +303,7 @@ export function useGetMetricHighlights<
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricHighlightsParams,
params?: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -328,7 +328,7 @@ export function useGetMetricHighlights<
*/
export const invalidateGetMetricHighlights = async (
queryClient: QueryClient,
params: GetMetricHighlightsParams,
params?: GetMetricHighlightsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
@@ -526,7 +526,7 @@ export const useGetMetricAttributes = <
* @summary Get metric metadata
*/
export const getMetricMetadata = (
params: GetMetricMetadataParams,
params?: GetMetricMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
@@ -547,7 +547,7 @@ export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricMetadataParams,
params?: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
@@ -585,7 +585,7 @@ export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = RenderErrorResponseDTO
>(
params: GetMetricMetadataParams,
params?: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
@@ -610,7 +610,7 @@ export function useGetMetricMetadata<
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
params: GetMetricMetadataParams,
params?: GetMetricMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(

View File

@@ -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);
};

View File

@@ -650,11 +650,11 @@ export interface MetricsexplorertypesMetricAlertDTO {
/**
* @type string
*/
alertId: string;
alertId?: string;
/**
* @type string
*/
alertName: string;
alertName?: string;
}
export interface MetricsexplorertypesMetricAlertsResponseDTO {
@@ -662,24 +662,24 @@ export interface MetricsexplorertypesMetricAlertsResponseDTO {
* @type array
* @nullable true
*/
alerts: MetricsexplorertypesMetricAlertDTO[] | null;
alerts?: MetricsexplorertypesMetricAlertDTO[] | null;
}
export interface MetricsexplorertypesMetricAttributeDTO {
/**
* @type string
*/
key: string;
key?: string;
/**
* @type integer
* @minimum 0
*/
valueCount: number;
valueCount?: number;
/**
* @type array
* @nullable true
*/
values: string[] | null;
values?: string[] | null;
}
export interface MetricsexplorertypesMetricAttributesRequestDTO {
@@ -691,7 +691,7 @@ export interface MetricsexplorertypesMetricAttributesRequestDTO {
/**
* @type string
*/
metricName: string;
metricName?: string;
/**
* @type integer
* @nullable true
@@ -704,31 +704,31 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
* @type array
* @nullable true
*/
attributes: MetricsexplorertypesMetricAttributeDTO[] | null;
attributes?: MetricsexplorertypesMetricAttributeDTO[] | null;
/**
* @type integer
* @format int64
*/
totalKeys: number;
totalKeys?: number;
}
export interface MetricsexplorertypesMetricDashboardDTO {
/**
* @type string
*/
dashboardId: string;
dashboardId?: string;
/**
* @type string
*/
dashboardName: string;
dashboardName?: string;
/**
* @type string
*/
widgetId: string;
widgetId?: string;
/**
* @type string
*/
widgetName: string;
widgetName?: string;
}
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
@@ -736,7 +736,7 @@ export interface MetricsexplorertypesMetricDashboardsResponseDTO {
* @type array
* @nullable true
*/
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
dashboards?: MetricsexplorertypesMetricDashboardDTO[] | null;
}
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
@@ -744,96 +744,74 @@ export interface MetricsexplorertypesMetricHighlightsResponseDTO {
* @type integer
* @minimum 0
*/
activeTimeSeries: number;
activeTimeSeries?: number;
/**
* @type integer
* @minimum 0
*/
dataPoints: number;
dataPoints?: number;
/**
* @type integer
* @minimum 0
*/
lastReceived: number;
lastReceived?: number;
/**
* @type integer
* @minimum 0
*/
totalTimeSeries: number;
totalTimeSeries?: number;
}
export enum MetricsexplorertypesMetricMetadataDTOTemporality {
delta = 'delta',
cumulative = 'cumulative',
unspecified = 'unspecified',
}
export enum MetricsexplorertypesMetricMetadataDTOType {
gauge = 'gauge',
sum = 'sum',
histogram = 'histogram',
summary = 'summary',
exponentialhistogram = 'exponentialhistogram',
}
export interface MetricsexplorertypesMetricMetadataDTO {
/**
* @type string
*/
description: string;
description?: string;
/**
* @type boolean
*/
isMonotonic: boolean;
/**
* @enum delta,cumulative,unspecified
* @type string
*/
temporality: MetricsexplorertypesMetricMetadataDTOTemporality;
/**
* @enum gauge,sum,histogram,summary,exponentialhistogram
* @type string
*/
type: MetricsexplorertypesMetricMetadataDTOType;
isMonotonic?: boolean;
/**
* @type string
*/
unit: string;
temporality?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
*/
unit?: string;
}
export enum MetricsexplorertypesStatDTOType {
gauge = 'gauge',
sum = 'sum',
histogram = 'histogram',
summary = 'summary',
exponentialhistogram = 'exponentialhistogram',
}
export interface MetricsexplorertypesStatDTO {
/**
* @type string
*/
description: string;
description?: string;
/**
* @type string
*/
metricName: string;
metricName?: string;
/**
* @type integer
* @minimum 0
*/
samples: number;
samples?: number;
/**
* @type integer
* @minimum 0
*/
timeseries: number;
/**
* @enum gauge,sum,histogram,summary,exponentialhistogram
* @type string
*/
type: MetricsexplorertypesStatDTOType;
timeseries?: number;
/**
* @type string
*/
unit: string;
type?: string;
/**
* @type string
*/
unit?: string;
}
export interface MetricsexplorertypesStatsRequestDTO {
@@ -841,12 +819,12 @@ export interface MetricsexplorertypesStatsRequestDTO {
* @type integer
* @format int64
*/
end: number;
end?: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type integer
*/
limit: number;
limit?: number;
/**
* @type integer
*/
@@ -856,7 +834,7 @@ export interface MetricsexplorertypesStatsRequestDTO {
* @type integer
* @format int64
*/
start: number;
start?: number;
}
export interface MetricsexplorertypesStatsResponseDTO {
@@ -864,56 +842,51 @@ export interface MetricsexplorertypesStatsResponseDTO {
* @type array
* @nullable true
*/
metrics: MetricsexplorertypesStatDTO[] | null;
metrics?: MetricsexplorertypesStatDTO[] | null;
/**
* @type integer
* @minimum 0
*/
total: number;
total?: number;
}
export interface MetricsexplorertypesTreemapEntryDTO {
/**
* @type string
*/
metricName: string;
metricName?: string;
/**
* @type number
* @format double
*/
percentage: number;
percentage?: number;
/**
* @type integer
* @minimum 0
*/
totalValue: number;
totalValue?: number;
}
export enum MetricsexplorertypesTreemapRequestDTOMode {
timeseries = 'timeseries',
samples = 'samples',
}
export interface MetricsexplorertypesTreemapRequestDTO {
/**
* @type integer
* @format int64
*/
end: number;
end?: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type integer
*/
limit: number;
limit?: number;
/**
* @enum timeseries,samples
* @type string
*/
mode: MetricsexplorertypesTreemapRequestDTOMode;
mode?: string;
/**
* @type integer
* @format int64
*/
start: number;
start?: number;
}
export interface MetricsexplorertypesTreemapResponseDTO {
@@ -921,53 +894,39 @@ export interface MetricsexplorertypesTreemapResponseDTO {
* @type array
* @nullable true
*/
samples: MetricsexplorertypesTreemapEntryDTO[] | null;
samples?: MetricsexplorertypesTreemapEntryDTO[] | null;
/**
* @type array
* @nullable true
*/
timeseries: MetricsexplorertypesTreemapEntryDTO[] | null;
timeseries?: MetricsexplorertypesTreemapEntryDTO[] | null;
}
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality {
delta = 'delta',
cumulative = 'cumulative',
unspecified = 'unspecified',
}
export enum MetricsexplorertypesUpdateMetricMetadataRequestDTOType {
gauge = 'gauge',
sum = 'sum',
histogram = 'histogram',
summary = 'summary',
exponentialhistogram = 'exponentialhistogram',
}
export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
/**
* @type string
*/
description: string;
description?: string;
/**
* @type boolean
*/
isMonotonic: boolean;
isMonotonic?: boolean;
/**
* @type string
*/
metricName: string;
/**
* @enum delta,cumulative,unspecified
* @type string
*/
temporality: MetricsexplorertypesUpdateMetricMetadataRequestDTOTemporality;
/**
* @enum gauge,sum,histogram,summary,exponentialhistogram
* @type string
*/
type: MetricsexplorertypesUpdateMetricMetadataRequestDTOType;
metricName?: string;
/**
* @type string
*/
unit: string;
temporality?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
*/
unit?: string;
}
export interface PreferencetypesPreferenceDTO {
@@ -1151,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
@@ -1384,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
@@ -1430,11 +1341,6 @@ export interface TypesPostableResetPasswordDTO {
}
export interface TypesResetPasswordTokenDTO {
/**
* @type string
* @format date-time
*/
expiresAt?: Date;
/**
* @type string
*/
@@ -1776,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
@@ -1892,19 +1762,6 @@ export type GetFeatures200 = {
status?: string;
};
export type GetIngestionKeysParams = {
/**
* @type integer
* @description undefined
*/
page?: number;
/**
* @type integer
* @description undefined
*/
per_page?: number;
};
export type GetIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
/**
@@ -1944,24 +1801,6 @@ export type DeleteIngestionKeyLimitPathParameters = {
export type UpdateIngestionKeyLimitPathParameters = {
limitId: string;
};
export type SearchIngestionKeysParams = {
/**
* @type string
* @description undefined
*/
name?: string;
/**
* @type integer
* @description undefined
*/
page?: number;
/**
* @type integer
* @description undefined
*/
per_page?: number;
};
export type SearchIngestionKeys200 = {
data?: GatewaytypesGettableIngestionKeysDTO;
/**
@@ -1975,7 +1814,7 @@ export type GetMetricAlertsParams = {
* @type string
* @description undefined
*/
metricName: string;
metricName?: string;
};
export type GetMetricAlerts200 = {
@@ -1991,7 +1830,7 @@ export type GetMetricDashboardsParams = {
* @type string
* @description undefined
*/
metricName: string;
metricName?: string;
};
export type GetMetricDashboards200 = {
@@ -2007,7 +1846,7 @@ export type GetMetricHighlightsParams = {
* @type string
* @description undefined
*/
metricName: string;
metricName?: string;
};
export type GetMetricHighlights200 = {
@@ -2034,7 +1873,7 @@ export type GetMetricMetadataParams = {
* @type string
* @description undefined
*/
metricName: string;
metricName?: string;
};
export type GetMetricMetadata200 = {

View File

@@ -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);
};

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
);

View File

@@ -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}
/>
))
)}

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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>
)}

View File

@@ -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();
});
});
});

View File

@@ -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
}
};

View File

@@ -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);
});
});

View File

@@ -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();
});
});

View File

@@ -54,5 +54,4 @@ export enum QueryParams {
version = 'version',
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
}

View File

@@ -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;

View File

@@ -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),
});
});
});

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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;
}
};

View File

@@ -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,
};
}

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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}

View File

@@ -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>
);
}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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 => {

View File

@@ -1,97 +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';
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 }): 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}
syncKey={syncKey}
render={(props: TooltipRenderArgs): React.ReactNode => (
<Tooltip
{...props}
timezone={timezone}
yAxisUnit={yAxisUnit}
decimalPrecision={decimalPrecision}
/>
)}
/>
)}
</UPlotChart>
)}
</ChartLayout>
</PlotContextProvider>
);
}

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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;
}
}

View File

@@ -1,74 +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;
}) => 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,
})}
</div>
<div
className="chart-layout__legend-wrapper"
style={{
height: chartDimensions.legendHeight,
width: chartDimensions.legendWidth,
}}
>
{legendComponent(chartDimensions.averageLegendWidth)}
</div>
</div>
{layoutChildren}
</div>
);
}

View File

@@ -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',
}

View File

@@ -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
}
}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 };
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -58,7 +58,7 @@
overflow-y: auto;
box-sizing: border-box;
height: calc(100% - 54px);
height: calc(100% - 64px);
&::-webkit-scrollbar {
height: 1rem;

View File

@@ -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';

View File

@@ -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;
}
}
}

View File

@@ -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;
}
`;

View File

@@ -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;

View File

@@ -1,7 +1,4 @@
.dashboard-navigation {
.run-query-dashboard-btn {
min-width: 180px;
}
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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),
};
});

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -15,7 +15,6 @@ import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { DOCS_BASE_URL } from 'constants/app';
import ROUTES from 'constants/routes';
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import history from 'lib/history';
import { isEmpty } from 'lodash-es';
@@ -149,8 +148,6 @@ function OnboardingAddDataSource(): JSX.Element {
const { org } = useAppContext();
const { data: globalConfig } = useGetGlobalConfig();
const [setupStepItems, setSetupStepItems] = useState(setupStepItemsBase);
const [searchQuery, setSearchQuery] = useState<string>('');
@@ -236,16 +233,6 @@ function OnboardingAddDataSource(): JSX.Element {
urlObj.searchParams.set('environment', selectedEnvironment);
}
const ingestionUrl = globalConfig?.data?.ingestion_url;
if (ingestionUrl) {
const parts = ingestionUrl.split('.');
if (parts?.length > 1 && parts[0]?.includes('ingest')) {
const region = parts[1];
urlObj.searchParams.set('region', region);
}
}
// Step 3: Return the updated URL as a string
const updatedUrl = urlObj.toString();

View File

@@ -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}

View File

@@ -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 => {}}

View File

@@ -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 => {}}

View File

@@ -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 = {

View File

@@ -1,17 +1,7 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { screen, within } from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { screen } from '@testing-library/react';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import {
findByText,
fireEvent,
render,
userEvent,
waitFor,
} from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { findByText, fireEvent, render, waitFor } from 'tests/test-utils';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
import PipelineListsView from '../PipelineListsView';
@@ -85,20 +75,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
}),
}));
const BASE_URL = ENVIRONMENT.baseURL;
const attributeKeysURL = `${BASE_URL}/api/v3/autocomplete/attribute_keys`;
describe('PipelinePage container test', () => {
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
jest.clearAllMocks();
});
afterAll(() => {
server.close();
});
it('should render PipelineListsView section', () => {
const { getByText, container } = render(
<PreferenceContextProvider>
@@ -295,7 +272,6 @@ describe('PipelinePage container test', () => {
});
it('should have populated form fields when edit pipeline is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<PreferenceContextProvider>
<PipelineListsView
@@ -325,52 +301,5 @@ describe('PipelinePage container test', () => {
// to have length 2
expect(screen.queryAllByText('source = nginx').length).toBe(2);
server.use(
rest.get(attributeKeysURL, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
attributeKeys: [
{
key: 'otelServiceName',
dataType: DataTypes.String,
type: 'tag',
},
{
key: 'service.instance.id',
dataType: DataTypes.String,
type: 'resource',
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'tag',
},
],
},
}),
),
),
);
// Open Filter input and type to trigger suggestions
const filterSelect = screen.getByTestId('qb-search-select');
const input = within(filterSelect).getByRole('combobox') as HTMLInputElement;
await user.click(input);
await waitFor(() =>
expect(screen.getByText('otelServiceName')).toBeInTheDocument(),
);
const serviceNameOccurences = await screen.findAllByText('service.name');
expect(serviceNameOccurences.length).toBeGreaterThanOrEqual(2);
});
});

View File

@@ -37,7 +37,6 @@ export type QueryBuilderProps = {
onChangeTraceView?: (view: TraceView) => void;
onSignalSourceChange?: (value: string) => void;
signalSourceChangeEnabled?: boolean;
savePreviousQuery?: boolean;
};
export enum TraceView {

View File

@@ -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;

View File

@@ -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'}

View File

@@ -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} />}
>

View File

@@ -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(

View File

@@ -356,10 +356,7 @@ function QueryBuilderSearch({
// conditional changes here to use a seperate component to render the example queries based on the option group label
const customRendererForLogsExplorer = options.map((option) => (
<Select.Option
key={`${option.label}-${option.type || ''}-${option.dataType || ''}`}
value={option.value}
>
<Select.Option key={option.label} value={option.value}>
<OptionRendererForLogs
label={option.label}
value={option.value}
@@ -374,7 +371,6 @@ function QueryBuilderSearch({
return (
<div className="query-builder-search-container">
<Select
data-testid={'qb-search-select'}
ref={selectRef}
getPopupContainer={popupContainer}
transitionName=""
@@ -492,10 +488,7 @@ function QueryBuilderSearch({
{isLogsExplorerPage
? customRendererForLogsExplorer
: options.map((option) => (
<Select.Option
key={`${option.label}-${option.type || ''}-${option.dataType || ''}`}
value={option.value}
>
<Select.Option key={option.label} value={option.value}>
<OptionRenderer
label={option.label}
value={option.value}

View File

@@ -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

View File

@@ -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,
}),
}));

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;');

Some files were not shown because too many files have changed in this diff Show More