Compare commits

..

11 Commits

Author SHA1 Message Date
Piyush Singariya
c0deb2a935 fix: tests 2026-02-02 13:48:18 +05:30
Piyush Singariya
e7923b757d Merge branch 'main' into json-qb-integration 2026-02-02 12:47:22 +05:30
Piyush Singariya
a7bc55bac9 revert: old json QB 2026-01-22 12:53:47 +05:30
Piyush Singariya
55ead8fb20 test: integration tests fixed 2026-01-21 22:34:22 +05:30
Piyush Singariya
0210f02397 Merge branch 'has-jsonqb' into json-qb-integration 2026-01-20 14:28:57 +05:30
Piyush Singariya
e8e4897cc8 fix: tests GroupBy 2026-01-20 12:10:44 +05:30
Piyush Singariya
96fb88aaee fix: ignored .vscode in gitignore 2026-01-20 11:47:34 +05:30
Piyush Singariya
5a00e6c2cd Merge branch 'main' into has-jsonqb 2026-01-20 11:44:32 +05:30
Piyush Singariya
e2500cff7d fix: tests expected queries and values 2026-01-20 11:40:56 +05:30
Piyush Singariya
4864c3bc37 feat: has JSON QB 2026-01-20 11:23:50 +05:30
Piyush Singariya
d5ac192dc4 feat: integration tests for new JSON QB 2026-01-19 12:22:46 +05:30
231 changed files with 4122 additions and 14644 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

@@ -2226,12 +2226,6 @@ paths:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unprocessable Entity
"500":
content:
application/json:
@@ -2291,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:
@@ -2612,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:
@@ -2671,7 +2643,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2726,7 +2697,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2782,7 +2752,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -2949,7 +2918,6 @@ paths:
parameters:
- in: query
name: metricName
required: true
schema:
type: string
responses:
@@ -3817,9 +3785,6 @@ components:
type: string
alertName:
type: string
required:
- alertName
- alertId
type: object
MetricsexplorertypesMetricAlertsResponse:
properties:
@@ -3828,8 +3793,6 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesMetricAlert'
nullable: true
type: array
required:
- alerts
type: object
MetricsexplorertypesMetricAttribute:
properties:
@@ -3843,10 +3806,6 @@ components:
type: string
nullable: true
type: array
required:
- key
- values
- valueCount
type: object
MetricsexplorertypesMetricAttributesRequest:
properties:
@@ -3858,8 +3817,6 @@ components:
start:
nullable: true
type: integer
required:
- metricName
type: object
MetricsexplorertypesMetricAttributesResponse:
properties:
@@ -3871,9 +3828,6 @@ components:
totalKeys:
format: int64
type: integer
required:
- attributes
- totalKeys
type: object
MetricsexplorertypesMetricDashboard:
properties:
@@ -3885,11 +3839,6 @@ components:
type: string
widgetName:
type: string
required:
- dashboardName
- dashboardId
- widgetId
- widgetName
type: object
MetricsexplorertypesMetricDashboardsResponse:
properties:
@@ -3898,8 +3847,6 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
nullable: true
type: array
required:
- dashboards
type: object
MetricsexplorertypesMetricHighlightsResponse:
properties:
@@ -3915,11 +3862,6 @@ components:
totalTimeSeries:
minimum: 0
type: integer
required:
- dataPoints
- lastReceived
- totalTimeSeries
- activeTimeSeries
type: object
MetricsexplorertypesMetricMetadata:
properties:
@@ -3928,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:
@@ -3963,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:
@@ -3996,10 +3909,6 @@ components:
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
MetricsexplorertypesStatsResponse:
properties:
@@ -4011,9 +3920,6 @@ components:
total:
minimum: 0
type: integer
required:
- metrics
- total
type: object
MetricsexplorertypesTreemapEntry:
properties:
@@ -4025,10 +3931,6 @@ components:
totalValue:
minimum: 0
type: integer
required:
- metricName
- percentage
- totalValue
type: object
MetricsexplorertypesTreemapRequest:
properties:
@@ -4040,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:
@@ -4065,9 +3959,6 @@ components:
$ref: '#/components/schemas/MetricsexplorertypesTreemapEntry'
nullable: true
type: array
required:
- timeseries
- samples
type: object
MetricsexplorertypesUpdateMetricMetadataRequest:
properties:
@@ -4078,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:
@@ -4382,9 +4256,6 @@ components:
type: string
orgId:
type: string
required:
- orgId
- email
type: object
TypesPostableInvite:
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

@@ -61,8 +61,6 @@ module.exports = {
curly: 'error', // Requires curly braces for all control statements
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
// TODO: Change this to error in May 2026
'max-params': ['warn', 3], // a function can have max 3 params after which it should become an object
// TypeScript rules
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
@@ -118,7 +116,7 @@ module.exports = {
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }], // Prevents importing packages not in package.json
'import/no-cycle': 'warn', // Warns about circular dependencies
// 'import/no-cycle': 'warn', // TODO: Enable later to detect circular dependencies
// Import sorting rules
'simple-import-sort/imports': [
@@ -148,19 +146,6 @@ module.exports = {
'sonarjs/no-duplicate-string': 'off', // Disabled - can be noisy (enable periodically to check)
},
overrides: [
{
files: [
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/__tests__/**/*.{js,jsx,ts,tsx}',
],
rules: {
// Tests often have intentional duplication and complexity - disable SonarJS rules
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
},
},
{
files: ['src/api/generated/**/*.ts'],
rules: {
@@ -168,6 +153,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-nested-ternary': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'sonarjs/no-duplicate-string': 'off',
},
},
],

View File

@@ -2,11 +2,6 @@
Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion.
### Export Style
- **React components** (`src/components/`, `src/container/`, `src/pages/`): Prefer **default exports** for the main component in each file
- **Utilities, hooks, APIs, types, constants** (`src/utils/`, `src/hooks/`, `src/api/`, `src/lib/`, `src/types/`, `src/constants/`): Prefer **named exports** for better tree-shaking and explicit imports
### React and Components
- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability.

View File

@@ -12,8 +12,6 @@ export interface MockUPlotInstance {
export interface MockUPlotPaths {
spline: jest.Mock;
bars: jest.Mock;
linear: jest.Mock;
stepped: jest.Mock;
}
// Create mock instance methods
@@ -25,20 +23,10 @@ const createMockUPlotInstance = (): MockUPlotInstance => ({
setSeries: jest.fn(),
});
// Path builder: (self, seriesIdx, idx0, idx1) => paths or null
const createMockPathBuilder = (): jest.Mock =>
jest.fn(() => ({
stroke: jest.fn(),
fill: jest.fn(),
clip: jest.fn(),
}));
// Create mock paths - linear, spline, stepped needed by UPlotSeriesBuilder.getPathBuilder
const mockPaths = {
spline: jest.fn(() => createMockPathBuilder()),
bars: jest.fn(() => createMockPathBuilder()),
linear: jest.fn(() => createMockPathBuilder()),
stepped: jest.fn((opts?: { align?: number }) => createMockPathBuilder()),
// Create mock paths
const mockPaths: MockUPlotPaths = {
spline: jest.fn(),
bars: jest.fn(),
};
// Mock static methods

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

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,53 +0,0 @@
import { memo, useMemo } from 'react';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import SelectVariableInput from './SelectVariableInput';
import { useDashboardVariableSelectHelper } from './useDashboardVariableSelectHelper';
import { VariableItemProps } from './VariableItem';
type CustomVariableInputProps = Pick<
VariableItemProps,
'variableData' | 'onValueUpdate'
>;
function CustomVariableInput({
variableData,
onValueUpdate,
}: CustomVariableInputProps): JSX.Element {
const optionsData: (string | number | boolean)[] = useMemo(() => {
return sortValues(
commaValuesParser(variableData.customValue || ''),
variableData.sort,
) as (string | number | boolean)[];
}, [variableData.customValue, variableData.sort]);
const {
value,
defaultValue,
enableSelectAll,
onChange,
onDropdownVisibleChange,
handleClear,
} = useDashboardVariableSelectHelper({
variableData,
optionsData,
onValueUpdate,
});
return (
<SelectVariableInput
variableId={variableData.id}
options={optionsData}
value={value}
onChange={onChange}
onDropdownVisibleChange={onDropdownVisibleChange}
onClear={handleClear}
enableSelectAll={enableSelectAll}
defaultValue={defaultValue}
isMultiSelect={variableData.multiSelect}
/>
);
}
export default memo(CustomVariableInput);

View File

@@ -25,6 +25,12 @@
}
}
&.focused {
.variable-value {
outline: 1px solid var(--bg-robin-400);
}
}
.variable-value {
display: flex;
min-width: 120px;
@@ -42,11 +48,6 @@
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
&:hover,
&:focus-within {
outline: 1px solid var(--bg-robin-400);
}
}
.variable-select {
@@ -98,6 +99,12 @@
.lightMode {
.variable-item {
&.focused {
.variable-value {
border: 1px solid var(--bg-robin-400);
}
}
.variable-name {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
@@ -108,11 +115,6 @@
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
color: var(--bg-ink-400);
&:hover,
&:focus-within {
outline: 1px solid var(--bg-robin-400);
}
}
}
}
@@ -122,9 +124,3 @@
padding: 4px 12px;
font-size: 12px;
}
.dashboard-variables-selection-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
}

View File

@@ -1,12 +1,9 @@
import { memo, useCallback, useEffect, useMemo } 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';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { AppState } from 'store/reducers';
@@ -14,13 +11,20 @@ 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';
function DashboardVariableSelection(): JSX.Element | null {
const {
selectedDashboard,
setSelectedDashboard,
updateLocalStorageDashboardVariables,
variablesToGetUpdated,
@@ -29,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>(
@@ -42,44 +48,82 @@ 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 = [];
// Memoize the order key to avoid unnecessary triggers
const dependencyOrderKey = useMemo(
() => dependencyData?.order?.join(',') ?? '',
[dependencyData?.order],
);
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { id } = value;
// Trigger refetch when dependency order changes or global time changes
useEffect(() => {
if (dependencyData?.order && dependencyData.order.length > 0) {
setVariablesToGetUpdated(dependencyData?.order || []);
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
}
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
// Initialize variables with default values if not in URL
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
}, [getUrlVariables, updateUrlVariable, variables]);
useEffect(() => {
if (variablesTableData.length > 0) {
const depGrp = buildDependencies(variablesTableData);
const { order, graph, hasCycle, cycleNodes } = buildDependencyGraph(depGrp);
const parentDependencyGraph = buildParentDependencyGraph(graph);
// cleanup order to only include variables that are of type 'QUERY'
const cleanedOrder = order.filter((variable) => {
const variableData = variablesTableData.find(
(v: IDashboardVariable) => v.name === variable,
);
return variableData?.type === 'QUERY';
});
setDependencyData({
order: cleanedOrder,
graph,
parentDependencyGraph,
hasCycle,
cycleNodes,
});
}
}, [variables, variablesTableData]);
// this handles the case where the dependency order changes i.e. variable list updated via creation or deletion etc. and we need to refetch the variables
// also trigger when the global time changes
useEffect(
() => {
if (!isEmpty(dependencyData?.order)) {
setVariablesToGetUpdated(dependencyData?.order || []);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dependencyOrderKey, minTime, maxTime]);
[JSON.stringify(dependencyData?.order), minTime, maxTime],
);
// Performance optimization: For dynamic variables with allSelected=true, we don't store
// individual values in localStorage since we can always derive them from available options.
// This makes localStorage much lighter and more efficient.
const onValueUpdate = useCallback(
(
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
const onValueUpdate = (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
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 isDynamic = variable.type === 'DYNAMIC';
const variable = variables?.[id] || variables?.[name];
const isDynamic = variable?.type === 'DYNAMIC';
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
if (allSelected) {
@@ -88,39 +132,41 @@ function DashboardVariableSelection(): JSX.Element | null {
updateUrlVariable(name || id, value);
}
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = { ...prev?.data.variables };
// this is added to handle case where we have two different
// schemas for variable response
if (oldVariables?.[id]) {
oldVariables[id] = {
...oldVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (oldVariables?.[name]) {
oldVariables[name] = {
...oldVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
return {
...prev,
data: {
...prev?.data,
variables: {
...oldVariables,
if (selectedDashboard) {
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = prev?.data.variables;
// this is added to handle case where we have two different
// schemas for variable response
if (oldVariables?.[id]) {
oldVariables[id] = {
...oldVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (oldVariables?.[name]) {
oldVariables[name] = {
...oldVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
return {
...prev,
data: {
...prev?.data,
variables: {
...oldVariables,
},
},
},
};
}
return prev;
});
};
}
return prev;
});
}
if (dependencyData) {
const updatedVariables: string[] = [];
@@ -136,42 +182,48 @@ function DashboardVariableSelection(): JSX.Element | null {
} else {
setVariablesToGetUpdated((prev) => prev.filter((v) => v !== name));
}
},
[
// This can be removed
dashboardVariables,
updateLocalStorageDashboardVariables,
dependencyData,
updateUrlVariable,
setSelectedDashboard,
setVariablesToGetUpdated,
],
}
};
if (!variables) {
return null;
}
const orderBasedSortedVariables = variablesTableData.sort(
(a: { order: number }, b: { order: number }) => a.order - b.order,
);
return (
<Row className="dashboard-variables-selection-container">
{sortedVariablesArray.map((variable) => {
const key = `${variable.name}${variable.id}${variable.order}`;
return variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={key}
existingVariables={dashboardVariables}
variableData={variable}
onValueUpdate={onValueUpdate}
/>
) : (
<VariableItem
key={key}
existingVariables={dashboardVariables}
variableData={variable}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
);
})}
<Row style={{ display: 'flex', gap: '12px' }}>
{orderBasedSortedVariables &&
Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) =>
variable.type === 'DYNAMIC' ? (
<DynamicVariableSelection
key={`${variable.name}${variable.id}${variable.order}`}
existingVariables={variables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
/>
) : (
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables}
variableData={{
name: variable.name,
...variable,
}}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
),
)}
</Row>
);
}

View File

@@ -23,9 +23,9 @@ import { SelectItemStyle } from './styles';
import {
areArraysEqual,
getOptionsForDynamicVariable,
getSelectValue,
uniqueValues,
} from './util';
import { getSelectValue } from './VariableItem';
import './DashboardVariableSelection.styles.scss';

View File

@@ -1,229 +0,0 @@
import { memo, useCallback, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { isArray, 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';
import { GlobalReducer } from 'types/reducer/globalTime';
import { variablePropsToPayloadVariables } from '../utils';
import SelectVariableInput from './SelectVariableInput';
import { useDashboardVariableSelectHelper } from './useDashboardVariableSelectHelper';
import { areArraysEqual, checkAPIInvocation } from './util';
interface QueryVariableInputProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void;
variablesToGetUpdated: string[];
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
dependencyData: IDependencyData | null;
}
function QueryVariableInput({
variableData,
existingVariables,
variablesToGetUpdated,
setVariablesToGetUpdated,
dependencyData,
onValueUpdate,
}: QueryVariableInputProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const {
tempSelection,
setTempSelection,
value,
defaultValue,
enableSelectAll,
onChange,
onDropdownVisibleChange,
handleClear,
} = useDashboardVariableSelectHelper({
variableData,
optionsData,
onValueUpdate,
});
const validVariableUpdate = (): boolean => {
if (!variableData.name) {
return false;
}
return Boolean(
variablesToGetUpdated.length &&
variablesToGetUpdated[0] === variableData.name,
);
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const getOptions = (variablesRes: VariableResponseProps | null): void => {
try {
setErrorMessage(null);
if (
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
) {
const newOptionsData = sortValues(
variablesRes?.variableValues,
variableData.sort,
);
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
let valueNotInList = false;
if (isArray(variableData.selectedValue)) {
variableData.selectedValue.forEach((val) => {
if (!newOptionsData.includes(val)) {
valueNotInList = true;
}
});
} else if (
isString(variableData.selectedValue) &&
!newOptionsData.includes(variableData.selectedValue)
) {
valueNotInList = true;
}
// variablesData.allSelected is added for the case where on change of options we need to update the
// local storage
if (
variableData.name &&
(validVariableUpdate() || valueNotInList || variableData.allSelected)
) {
if (
variableData.allSelected &&
variableData.multiSelect &&
variableData.showALLOption
) {
onValueUpdate(variableData.name, variableData.id, newOptionsData, true);
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(newOptionsData.map((option) => option.toString()));
}
} else {
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
Array.isArray(selectedValue) &&
newOptionsData.every((option) => selectedValue.includes(option));
}
if (variableData.name && variableData.id) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
}
setOptionsData(newOptionsData);
} else {
setVariablesToGetUpdated((prev) =>
prev.filter((name) => name !== variableData.name),
);
}
}
} catch (e) {
console.error(e);
}
};
const { isLoading, refetch } = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableData.name || '',
`${minTime}`,
`${maxTime}`,
JSON.stringify(dependencyData?.order),
],
{
enabled:
variableData &&
variableData.type === 'QUERY' &&
checkAPIInvocation(
variablesToGetUpdated,
variableData,
dependencyData?.parentDependencyGraph,
),
queryFn: () =>
dashboardVariablesQuery({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
getOptions(response.payload);
setVariablesToGetUpdated((prev) =>
prev.filter((v) => v !== variableData.name),
);
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if ((details.error ?? '').toString().includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorMessage(message);
}
setVariablesToGetUpdated((prev) =>
prev.filter((v) => v !== variableData.name),
);
},
},
);
const handleRetry = useCallback((): void => {
setErrorMessage(null);
refetch();
}, [refetch]);
return (
<SelectVariableInput
variableId={variableData.id}
options={optionsData}
value={value}
onChange={onChange}
onDropdownVisibleChange={onDropdownVisibleChange}
onClear={handleClear}
enableSelectAll={enableSelectAll}
defaultValue={defaultValue}
isMultiSelect={variableData.multiSelect}
// query variable specific, API related props
loading={isLoading}
errorMessage={errorMessage}
onRetry={handleRetry}
/>
);
}
export default memo(QueryVariableInput);

View File

@@ -1,134 +0,0 @@
import { memo, useMemo } from 'react';
import { orange } from '@ant-design/colors';
import { WarningOutlined } from '@ant-design/icons';
import { Popover, Tooltip, Typography } from 'antd';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import { popupContainer } from 'utils/selectPopupContainer';
import { ALL_SELECT_VALUE } from '../utils';
import { SelectItemStyle } from './styles';
const errorIconStyle = { margin: '0 0.5rem' };
interface SelectVariableInputProps {
variableId: string;
options: (string | number | boolean)[];
value: string | string[] | undefined;
enableSelectAll: boolean;
isMultiSelect: boolean;
onChange: (value: string | string[]) => void;
onClear: () => void;
defaultValue?: string | string[];
onDropdownVisibleChange?: (visible: boolean) => void;
loading?: boolean;
errorMessage?: string | null;
onRetry?: () => void;
}
const MAX_TAG_DISPLAY_VALUES = 10;
function maxTagPlaceholder(
omittedValues: { label?: React.ReactNode; value?: string | number }[],
): JSX.Element {
const valuesToShow = omittedValues.slice(0, MAX_TAG_DISPLAY_VALUES);
const hasMore = omittedValues.length > MAX_TAG_DISPLAY_VALUES;
const tooltipText =
valuesToShow.map(({ value: v }) => v ?? '').join(', ') +
(hasMore ? ` + ${omittedValues.length - MAX_TAG_DISPLAY_VALUES} more` : '');
return (
<Tooltip title={tooltipText}>
<span>+ {omittedValues.length} </span>
</Tooltip>
);
}
function SelectVariableInput({
variableId,
options,
value,
onChange,
onDropdownVisibleChange,
onClear,
loading,
errorMessage,
onRetry,
enableSelectAll,
isMultiSelect,
defaultValue,
}: SelectVariableInputProps): JSX.Element {
const selectOptions = useMemo(
() =>
options.map((option) => ({
label: option.toString(),
value: option.toString(),
})),
[options],
);
const commonProps = useMemo(
() => ({
// main props
key: variableId,
value,
defaultValue,
// setup props
placeholder: 'Select value',
className: 'variable-select',
popupClassName: 'dropdown-styles',
getPopupContainer: popupContainer,
style: SelectItemStyle,
showSearch: true,
bordered: false,
// dynamic props
'data-testid': 'variable-select',
onChange,
loading,
options: selectOptions,
errorMessage,
onRetry,
}),
[
variableId,
defaultValue,
onChange,
loading,
selectOptions,
value,
errorMessage,
onRetry,
],
);
return (
<>
{isMultiSelect ? (
<CustomMultiSelect
{...commonProps}
placement="bottomLeft"
maxTagCount={2}
onDropdownVisibleChange={onDropdownVisibleChange}
maxTagPlaceholder={maxTagPlaceholder}
onClear={onClear}
enableAllSelection={enableSelectAll}
maxTagTextLength={30}
allowClear={value !== ALL_SELECT_VALUE && value !== 'ALL'}
/>
) : (
<CustomSelect {...commonProps} />
)}
{errorMessage && (
<span style={errorIconStyle}>
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</>
);
}
export default memo(SelectVariableInput);

View File

@@ -1,85 +0,0 @@
import { memo, useCallback, useRef, useState } from 'react';
import { Input, InputRef } from 'antd';
import { VariableItemProps } from './VariableItem';
type TextboxVariableInputProps = Pick<
VariableItemProps,
'variableData' | 'onValueUpdate'
>;
function TextboxVariableInput({
variableData,
onValueUpdate,
}: TextboxVariableInputProps): JSX.Element {
const handleChange = useCallback(
(inputValue: string | string[]): void => {
if (inputValue === variableData.selectedValue) {
return;
}
if (variableData.name) {
onValueUpdate(variableData.name, variableData.id, inputValue, false);
}
},
[
onValueUpdate,
variableData.id,
variableData.name,
variableData.selectedValue,
],
);
const textboxInputRef = useRef<InputRef>(null);
const [textboxInputValue, setTextboxInputValue] = useState<string>(
(variableData.selectedValue?.toString() ||
variableData.defaultValue?.toString()) ??
'',
);
const handleInputOnChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setTextboxInputValue(event.target.value);
},
[setTextboxInputValue],
);
const handleInputOnBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>): void => {
const value = event.target.value.trim();
// If empty, reset to default value
if (!value && variableData.defaultValue) {
setTextboxInputValue(variableData.defaultValue.toString());
handleChange(variableData.defaultValue.toString());
} else {
handleChange(value);
}
},
[handleChange, variableData.defaultValue],
);
const handleInputOnKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
textboxInputRef.current?.blur();
}
},
[],
);
return (
<Input
key={variableData.id}
ref={textboxInputRef}
placeholder="Enter value"
data-testid={`variable-textbox-${variableData.id}`}
bordered={false}
value={textboxInputValue}
title={textboxInputValue}
onChange={handleInputOnChange}
onBlur={handleInputOnBlur}
onKeyDown={handleInputOnKeyDown}
/>
);
}
export default memo(TextboxVariableInput);

View File

@@ -1,16 +1,34 @@
import { memo } from 'react';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import { IDependencyData } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-nested-ternary */
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { orange } from '@ant-design/colors';
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
import { Input, InputRef, Popover, Tooltip, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isEmpty, isString } from 'lodash-es';
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import { popupContainer } from 'utils/selectPopupContainer';
import CustomVariableInput from './CustomVariableInput';
import QueryVariableInput from './QueryVariableInput';
import TextboxVariableInput from './TextboxVariableInput';
import { ALL_SELECT_VALUE, variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle } from './styles';
import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
import './DashboardVariableSelection.styles.scss';
export interface VariableItemProps {
interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: (
@@ -24,49 +42,488 @@ export interface VariableItemProps {
dependencyData: IDependencyData | null;
}
export const getSelectValue = (
selectedValue: IDashboardVariable['selectedValue'],
variableData: IDashboardVariable,
): string | string[] | undefined => {
if (Array.isArray(selectedValue)) {
if (!variableData.multiSelect && selectedValue.length === 1) {
return selectedValue[0]?.toString();
}
return selectedValue.map((item) => item.toString());
}
return selectedValue?.toString();
};
// eslint-disable-next-line sonarjs/cognitive-complexity
function VariableItem({
variableData,
onValueUpdate,
existingVariables,
onValueUpdate,
variablesToGetUpdated,
setVariablesToGetUpdated,
dependencyData,
}: VariableItemProps): JSX.Element {
const { name, description, type: variableType } = variableData;
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [tempSelection, setTempSelection] = useState<
string | string[] | undefined
>(undefined);
// Local state for textbox input to ensure smooth editing experience
const [textboxInputValue, setTextboxInputValue] = useState<string>(
(variableData.selectedValue?.toString() ||
variableData.defaultValue?.toString()) ??
'',
);
const [isTextboxFocused, setIsTextboxFocused] = useState<boolean>(false);
const textboxInputRef = useRef<InputRef>(null);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const validVariableUpdate = (): boolean => {
if (!variableData.name) {
return false;
}
// variableData.name is present as the top element or next in the queue - variablesToGetUpdated
return Boolean(
variablesToGetUpdated.length &&
variablesToGetUpdated[0] === variableData.name,
);
};
const [errorMessage, setErrorMessage] = useState<null | string>(null);
// eslint-disable-next-line sonarjs/cognitive-complexity
const getOptions = (variablesRes: VariableResponseProps | null): void => {
if (variablesRes && variableData.type === 'QUERY') {
try {
setErrorMessage(null);
if (
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
) {
const newOptionsData = sortValues(
variablesRes?.variableValues,
variableData.sort,
);
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
/* eslint-disable no-useless-escape */
let valueNotInList = false;
if (isArray(variableData.selectedValue)) {
variableData.selectedValue.forEach((val) => {
const isUsed = newOptionsData.includes(val);
if (!isUsed) {
valueNotInList = true;
}
});
} else if (isString(variableData.selectedValue)) {
const isUsed = newOptionsData.includes(variableData.selectedValue);
if (!isUsed) {
valueNotInList = true;
}
}
// variablesData.allSelected is added for the case where on change of options we need to update the
// local storage
if (
variableData.type === 'QUERY' &&
variableData.name &&
(validVariableUpdate() || valueNotInList || variableData.allSelected)
) {
if (
variableData.allSelected &&
variableData.multiSelect &&
variableData.showALLOption
) {
onValueUpdate(variableData.name, variableData.id, newOptionsData, true);
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(newOptionsData.map((option) => option.toString()));
}
} else {
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
Array.isArray(selectedValue) &&
newOptionsData.every((option) => selectedValue.includes(option));
}
if (variableData && variableData?.name && variableData?.id) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
}
setOptionsData(newOptionsData);
} else {
setVariablesToGetUpdated((prev) =>
prev.filter((name) => name !== variableData.name),
);
}
}
} catch (e) {
console.error(e);
}
} else if (variableData.type === 'CUSTOM') {
const optionsData = sortValues(
commaValuesParser(variableData.customValue || ''),
variableData.sort,
) as never;
setOptionsData(optionsData);
}
};
const { isLoading, refetch } = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
variableData.name || '',
`${minTime}`,
`${maxTime}`,
JSON.stringify(dependencyData?.order),
],
{
enabled:
variableData &&
variableData.type === 'QUERY' &&
checkAPIInvocation(
variablesToGetUpdated,
variableData,
dependencyData?.parentDependencyGraph,
),
queryFn: () =>
dashboardVariablesQuery({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
getOptions(response.payload);
setVariablesToGetUpdated((prev) =>
prev.filter((v) => v !== variableData.name),
);
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if ((details.error ?? '').toString().includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorMessage(message);
}
setVariablesToGetUpdated((prev) =>
prev.filter((v) => v !== variableData.name),
);
},
},
);
const handleChange = useCallback(
(inputValue: string | string[]): void => {
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
if (
value === variableData.selectedValue ||
(Array.isArray(value) &&
Array.isArray(variableData.selectedValue) &&
areArraysEqual(value, variableData.selectedValue))
) {
return;
}
if (variableData.name) {
// Check if ALL is effectively selected by comparing with available options
const isAllSelected =
Array.isArray(value) &&
value.length > 0 &&
optionsData.every((option) => value.includes(option.toString()));
if (isAllSelected && variableData.showALLOption) {
// For ALL selection, pass null to avoid storing values
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
onValueUpdate(variableData.name, variableData.id, value, false);
}
}
},
[
variableData.multiSelect,
variableData.selectedValue,
variableData.name,
variableData.id,
onValueUpdate,
optionsData,
variableData.showALLOption,
],
);
// Add a handler for tracking temporary selection changes
const handleTempChange = (inputValue: string | string[]): void => {
// Store the selection in temporary state while dropdown is open
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
setTempSelection(value);
};
// Handle dropdown visibility changes
const handleDropdownVisibleChange = (visible: boolean): void => {
// Initialize temp selection when opening dropdown
if (visible) {
setTempSelection(getSelectValue(variableData.selectedValue, variableData));
}
// Apply changes when closing dropdown
else if (!visible && tempSelection !== undefined) {
// Call handleChange with the temporarily stored selection
handleChange(tempSelection);
setTempSelection(undefined);
}
};
// do not debounce the above function as we do not need debounce in select variables
const debouncedHandleChange = debounce(handleChange, 500);
const { selectedValue } = variableData;
const selectedValueStringified = useMemo(
() => getSelectValue(selectedValue, variableData),
[selectedValue, variableData],
);
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
const selectValue =
variableData.allSelected && enableSelectAll
? 'ALL'
: selectedValueStringified;
// Apply default value on first render if no selection exists
// eslint-disable-next-line sonarjs/cognitive-complexity
const finalSelectedValues = useMemo(() => {
if (variableData.multiSelect) {
let value = tempSelection || selectedValue;
if (isEmpty(value)) {
if (variableData.showALLOption) {
if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData;
}
} else if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData?.[0];
}
}
return value;
}
if (isEmpty(selectedValue)) {
if (variableData.defaultValue) {
return variableData.defaultValue;
}
return optionsData[0]?.toString();
}
return selectedValue;
}, [
variableData.multiSelect,
variableData.showALLOption,
variableData.defaultValue,
selectedValue,
tempSelection,
optionsData,
]);
useEffect(() => {
if (
(variableData.multiSelect && !(tempSelection || selectValue)) ||
isEmpty(selectValue)
) {
handleChange(finalSelectedValues as string[] | string);
}
}, [
finalSelectedValues,
handleChange,
selectValue,
tempSelection,
variableData.multiSelect,
]);
useEffect(() => {
// Fetch options for CUSTOM Type
if (variableData.type === 'CUSTOM') {
getOptions(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variableData.type, variableData.customValue]);
return (
<div className="variable-item">
<div className={`variable-item${isTextboxFocused ? ' focused' : ''}`}>
<Typography.Text className="variable-name" ellipsis>
${name}
{description && (
<Tooltip title={description}>
${variableData.name}
{variableData.description && (
<Tooltip title={variableData.description}>
<InfoCircleOutlined className="info-icon" />
</Tooltip>
)}
</Typography.Text>
<div className="variable-value">
{variableType === 'TEXTBOX' && (
<TextboxVariableInput
variableData={variableData}
onValueUpdate={onValueUpdate}
{variableData.type === 'TEXTBOX' ? (
<Input
ref={textboxInputRef}
placeholder="Enter value"
data-testid={`variable-textbox-${variableData.id}`}
bordered={false}
value={textboxInputValue}
title={textboxInputValue}
onChange={(e): void => {
setTextboxInputValue(e.target.value);
}}
onFocus={(): void => {
setIsTextboxFocused(true);
}}
onBlur={(e): void => {
setIsTextboxFocused(false);
const value = e.target.value.trim();
// If empty, reset to default value
if (!value && variableData.defaultValue) {
setTextboxInputValue(variableData.defaultValue.toString());
debouncedHandleChange(variableData.defaultValue.toString());
} else {
debouncedHandleChange(value);
}
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
const value = textboxInputValue.trim();
if (!value && variableData.defaultValue) {
setTextboxInputValue(variableData.defaultValue.toString());
debouncedHandleChange(variableData.defaultValue.toString());
} else {
debouncedHandleChange(value);
}
textboxInputRef.current?.blur();
}
}}
/>
) : (
optionsData &&
(variableData.multiSelect ? (
<CustomMultiSelect
key={
selectValue && Array.isArray(selectValue)
? selectValue.join(' ')
: selectValue || variableData.id
}
options={optionsData.map((option) => ({
label: option.toString(),
value: option.toString(),
}))}
defaultValue={variableData.defaultValue || selectValue}
onChange={handleTempChange}
bordered={false}
placeholder="Select value"
placement="bottomLeft"
style={SelectItemStyle}
loading={isLoading}
showSearch
data-testid="variable-select"
className="variable-select"
popupClassName="dropdown-styles"
maxTagCount={2}
getPopupContainer={popupContainer}
value={tempSelection || selectValue}
onDropdownVisibleChange={handleDropdownVisibleChange}
errorMessage={errorMessage}
// eslint-disable-next-line react/no-unstable-nested-components
maxTagPlaceholder={(omittedValues): JSX.Element => {
const maxDisplayValues = 10;
const valuesToShow = omittedValues.slice(0, maxDisplayValues);
const hasMore = omittedValues.length > maxDisplayValues;
const tooltipText =
valuesToShow.map(({ value }) => value).join(', ') +
(hasMore ? ` + ${omittedValues.length - maxDisplayValues} more` : '');
return (
<Tooltip title={tooltipText}>
<span>+ {omittedValues.length} </span>
</Tooltip>
);
}}
onClear={(): void => {
handleChange([]);
}}
enableAllSelection={enableSelectAll}
maxTagTextLength={30}
allowClear={selectValue !== ALL_SELECT_VALUE && selectValue !== 'ALL'}
onRetry={(): void => {
setErrorMessage(null);
refetch();
}}
/>
) : (
<CustomSelect
key={
selectValue && Array.isArray(selectValue)
? selectValue.join(' ')
: selectValue || variableData.id
}
defaultValue={variableData.defaultValue || selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
style={SelectItemStyle}
loading={isLoading}
showSearch
data-testid="variable-select"
className="variable-select"
popupClassName="dropdown-styles"
getPopupContainer={popupContainer}
options={optionsData.map((option) => ({
label: option.toString(),
value: option.toString(),
}))}
value={selectValue}
errorMessage={errorMessage}
onRetry={(): void => {
setErrorMessage(null);
refetch();
}}
/>
))
)}
{variableType === 'CUSTOM' && (
<CustomVariableInput
variableData={variableData}
onValueUpdate={onValueUpdate}
/>
)}
{variableType === 'QUERY' && (
<QueryVariableInput
variableData={variableData}
onValueUpdate={onValueUpdate}
existingVariables={existingVariables}
variablesToGetUpdated={variablesToGetUpdated}
setVariablesToGetUpdated={setVariablesToGetUpdated}
dependencyData={dependencyData}
/>
{variableData.type !== 'TEXTBOX' && errorMessage && (
<span style={{ margin: '0 0.5rem' }}>
<Popover
placement="top"
content={<Typography>{errorMessage}</Typography>}
>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</div>
</div>

View File

@@ -1,201 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { isEmpty } from 'lodash-es';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { areArraysEqual, getSelectValue } from './util';
interface UseDashboardVariableSelectHelperParams {
variableData: IDashboardVariable;
optionsData: (string | number | boolean)[];
onValueUpdate: (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void;
}
interface UseDashboardVariableSelectHelperReturn {
// State
tempSelection: string | string[] | undefined;
setTempSelection: React.Dispatch<
React.SetStateAction<string | string[] | undefined>
>;
value: string | string[] | undefined;
defaultValue: string | string[] | undefined;
// Derived values
enableSelectAll: boolean;
// Handlers
onChange: (value: string | string[]) => void;
onDropdownVisibleChange: (visible: boolean) => void;
handleClear: () => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export function useDashboardVariableSelectHelper({
variableData,
optionsData,
onValueUpdate,
}: UseDashboardVariableSelectHelperParams): UseDashboardVariableSelectHelperReturn {
const { selectedValue } = variableData;
const [tempSelection, setTempSelection] = useState<
string | string[] | undefined
>(undefined);
const selectedValueStringified = useMemo(
() => getSelectValue(selectedValue, variableData),
[selectedValue, variableData],
);
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
const selectValue =
variableData.allSelected && enableSelectAll
? 'ALL'
: selectedValueStringified;
const handleChange = useCallback(
(inputValue: string | string[]): void => {
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
if (
value === variableData.selectedValue ||
(Array.isArray(value) &&
Array.isArray(variableData.selectedValue) &&
areArraysEqual(value, variableData.selectedValue))
) {
return;
}
if (variableData.name) {
// Check if ALL is effectively selected by comparing with available options
const isAllSelected =
Array.isArray(value) &&
value.length > 0 &&
optionsData.every((option) => value.includes(option.toString()));
if (isAllSelected && variableData.showALLOption) {
// For ALL selection, pass optionsData as the value and set allSelected to true
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
onValueUpdate(variableData.name, variableData.id, value, false);
}
}
},
[
variableData.multiSelect,
variableData.selectedValue,
variableData.name,
variableData.id,
variableData.showALLOption,
onValueUpdate,
optionsData,
],
);
const handleTempChange = useCallback(
(inputValue: string | string[]): void => {
// Store the selection in temporary state while dropdown is open
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
setTempSelection(value);
},
[variableData.multiSelect],
);
// Apply default value on first render if no selection exists
const finalSelectedValues = useMemo(() => {
if (variableData.multiSelect) {
let value = tempSelection || selectedValue;
if (isEmpty(value)) {
if (variableData.showALLOption) {
if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData;
}
} else if (variableData.defaultValue) {
value = variableData.defaultValue;
} else {
value = optionsData?.[0];
}
}
return value;
}
if (isEmpty(selectedValue)) {
if (variableData.defaultValue) {
return variableData.defaultValue;
}
return optionsData[0]?.toString();
}
return selectedValue;
}, [
variableData.multiSelect,
variableData.showALLOption,
variableData.defaultValue,
selectedValue,
tempSelection,
optionsData,
]);
// Apply default values when needed
useEffect(() => {
if (
(variableData.multiSelect && !(tempSelection || selectValue)) ||
isEmpty(selectValue)
) {
handleChange(finalSelectedValues as string[] | string);
}
}, [
finalSelectedValues,
handleChange,
selectValue,
tempSelection,
variableData.multiSelect,
]);
// Handle dropdown visibility changes
const onDropdownVisibleChange = useCallback(
(visible: boolean): void => {
// Initialize temp selection when opening dropdown
if (visible) {
setTempSelection(getSelectValue(variableData.selectedValue, variableData));
}
// Apply changes when closing dropdown
else if (!visible && tempSelection !== undefined) {
// Call handleChange with the temporarily stored selection
handleChange(tempSelection);
setTempSelection(undefined);
}
},
[variableData, tempSelection, handleChange],
);
const handleClear = useCallback((): void => {
handleChange([]);
}, [handleChange]);
const value = variableData.multiSelect
? tempSelection || selectValue
: selectValue;
const defaultValue = variableData.defaultValue || selectValue;
const onChange = useMemo(() => {
return variableData.multiSelect ? handleTempChange : handleChange;
}, [variableData.multiSelect, handleTempChange, handleChange]);
return {
tempSelection,
setTempSelection,
enableSelectAll,
onDropdownVisibleChange,
handleClear,
value,
defaultValue,
onChange,
};
}

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 => {
@@ -381,16 +385,3 @@ export const uniqueValues = (values: string[] | string): string[] | string => {
return values;
};
export const getSelectValue = (
selectedValue: IDashboardVariable['selectedValue'],
variableData: IDashboardVariable,
): string | string[] | undefined => {
if (Array.isArray(selectedValue)) {
if (!variableData.multiSelect && selectedValue.length === 1) {
return selectedValue[0]?.toString();
}
return selectedValue.map((item) => item.toString());
}
return selectedValue?.toString();
};

View File

@@ -1,104 +0,0 @@
import { useCallback, useRef } from 'react';
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
import Legend from 'lib/uPlotV2/components/Legend/Legend';
import Tooltip from 'lib/uPlotV2/components/Tooltip/Tooltip';
import {
LegendPosition,
TooltipRenderArgs,
} from 'lib/uPlotV2/components/types';
import UPlotChart from 'lib/uPlotV2/components/UPlotChart';
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
import _noop from 'lodash-es/noop';
import uPlot from 'uplot';
import { ChartProps } from '../types';
const TOOLTIP_WIDTH_PADDING = 60;
const TOOLTIP_MIN_WIDTH = 200;
export default function TimeSeries({
legendConfig = { position: LegendPosition.BOTTOM },
config,
data,
width: containerWidth,
height: containerHeight,
disableTooltip = false,
canPinTooltip = false,
timezone,
yAxisUnit,
decimalPrecision,
syncMode,
syncKey,
onDestroy = _noop,
children,
layoutChildren,
'data-testid': testId,
}: ChartProps): JSX.Element {
const plotInstanceRef = useRef<uPlot | null>(null);
const legendComponent = useCallback(
(averageLegendWidth: number): React.ReactNode => {
return (
<Legend
config={config}
position={legendConfig.position}
averageLegendWidth={averageLegendWidth}
/>
);
},
[config, legendConfig.position],
);
return (
<PlotContextProvider>
<ChartLayout
config={config}
containerWidth={containerWidth}
containerHeight={containerHeight}
legendConfig={legendConfig}
legendComponent={legendComponent}
layoutChildren={layoutChildren}
>
{({ chartWidth, chartHeight, averageLegendWidth }): JSX.Element => (
<UPlotChart
config={config}
data={data}
width={chartWidth}
height={chartHeight}
plotRef={(plot): void => {
plotInstanceRef.current = plot;
}}
onDestroy={(plot: uPlot): void => {
plotInstanceRef.current = null;
onDestroy(plot);
}}
data-testid={testId}
>
{children}
{!disableTooltip && (
<TooltipPlugin
config={config}
canPinTooltip={canPinTooltip}
syncMode={syncMode}
maxWidth={Math.max(
TOOLTIP_MIN_WIDTH,
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
)}
syncKey={syncKey}
render={(props: TooltipRenderArgs): React.ReactNode => (
<Tooltip
{...props}
timezone={timezone}
yAxisUnit={yAxisUnit}
decimalPrecision={decimalPrecision}
/>
)}
/>
)}
</UPlotChart>
)}
</ChartLayout>
</PlotContextProvider>
);
}

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-manager-container {
width: 100%;
max-height: calc(40% - 40px);
display: flex;
flex-direction: column;
gap: 16px;
.chart-manager-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.chart-manager-actions-container {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
}
}

View File

@@ -1,147 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Input } from 'antd';
import { ResizeTable } from 'components/ResizeTable';
import { getGraphManagerTableColumns } from 'container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns';
import { ExtendedChartDataset } from 'container/GridCardLayout/GridCard/FullView/types';
import { getDefaultTableDataSet } from 'container/GridCardLayout/GridCard/FullView/utils';
import { useNotifications } from 'hooks/useNotifications';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import './ChartManager.styles.scss';
interface ChartManagerProps {
config: UPlotConfigBuilder;
alignedData: uPlot.AlignedData;
yAxisUnit?: string;
onCancel?: () => void;
}
/**
* ChartManager provides a tabular view to manage the visibility of
* individual series on a uPlot chart.
*
* It syncs with the legend state coming from the plot context and
* allows users to:
* - filter series by label
* - toggle individual series on/off
* - persist the visibility configuration to local storage.
*
* @param config - `UPlotConfigBuilder` instance used to derive chart options.
* @param alignedData - uPlot aligned data used to build the initial table dataset.
* @param yAxisUnit - Optional unit label for Y-axis values shown in the table.
* @param onCancel - Optional callback invoked when the user cancels the dialog.
*/
export default function ChartManager({
config,
alignedData,
yAxisUnit,
onCancel,
}: ChartManagerProps): JSX.Element {
const { notifications } = useNotifications();
const { legendItemsMap } = useLegendsSync({
config,
subscribeToFocusChange: false,
});
const {
onToggleSeriesOnOff,
onToggleSeriesVisibility,
syncSeriesVisibilityToLocalStorage,
} = usePlotContext();
const { isDashboardLocked } = useDashboard();
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
);
const graphVisibilityState = useMemo(
() =>
Object.entries(legendItemsMap).reduce<boolean[]>((acc, [key, item]) => {
acc[Number(key)] = item.show;
return acc;
}, []),
[legendItemsMap],
);
useEffect(() => {
setTableDataSet(
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
);
}, [alignedData, config]);
const filterHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value.toString().toLowerCase();
const updatedDataSet = tableDataSet.map((item) => {
if (item.label?.toLocaleLowerCase().includes(value)) {
return { ...item, show: true };
}
return { ...item, show: false };
});
setTableDataSet(updatedDataSet);
},
[tableDataSet],
);
const dataSource = useMemo(
() =>
tableDataSet.filter(
(item, index) => index !== 0 && item.show, // skipping the first item as it is the x-axis
),
[tableDataSet],
);
const columns = useMemo(
() =>
getGraphManagerTableColumns({
tableDataSet,
checkBoxOnChangeHandler: (_e, index) => {
onToggleSeriesOnOff(index);
},
graphVisibilityState,
labelClickedHandler: onToggleSeriesVisibility,
yAxisUnit,
isGraphDisabled: isDashboardLocked,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[tableDataSet, graphVisibilityState, yAxisUnit, isDashboardLocked],
);
const handleSave = useCallback((): void => {
syncSeriesVisibilityToLocalStorage();
notifications.success({
message: 'The updated graphs & legends are saved',
});
if (onCancel) {
onCancel();
}
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
return (
<div className="chart-manager-container">
<div className="chart-manager-header">
<Input onChange={filterHandler} placeholder="Filter Series" />
<div className="chart-manager-actions-container">
<Button type="default" onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={handleSave}>
Save
</Button>
</div>
</div>
<div className="chart-manager-table-container">
<ResizeTable
columns={columns}
dataSource={dataSource}
virtual
rowKey="index"
scroll={{ y: 200 }}
pagination={false}
/>
</div>
</div>
);
}

View File

@@ -1,120 +0,0 @@
import { useCallback } from 'react';
import { UseQueryResult } from 'react-query';
import {
getTimeRangeFromStepInterval,
isApmMetric,
} from 'container/PanelWrapper/utils';
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import {
PopoverPosition,
useCoordinates,
} from 'periscope/components/ContextMenu';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataSource } from 'types/common/queryBuilder';
interface UseTimeSeriesContextMenuParams {
widget: Widgets;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
}
export const usePanelContextMenu = ({
widget,
queryResponse,
}: UseTimeSeriesContextMenuParams): {
coordinates: { x: number; y: number } | null;
popoverPosition: PopoverPosition | null;
onClose: () => void;
menuItemsConfig: {
header?: string | React.ReactNode;
items?: React.ReactNode;
};
clickHandlerWithContextMenu: (...args: any[]) => void;
} => {
const {
coordinates,
popoverPosition,
clickedData,
onClose,
subMenu,
onClick,
setSubMenu,
} = useCoordinates();
const { menuItemsConfig } = useGraphContextMenu({
widgetId: widget.id || '',
query: widget.query,
graphData: clickedData,
onClose,
coordinates,
subMenu,
setSubMenu,
contextLinks: widget.contextLinks,
panelType: widget.panelTypes,
queryRange: queryResponse,
});
const clickHandlerWithContextMenu = useCallback(
(...args: any[]) => {
const [
xValue,
_yvalue,
_mouseX,
_mouseY,
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
axesData,
focusedSeries,
] = args;
const data = getUplotClickData({
metric,
queryData,
absoluteMouseX,
absoluteMouseY,
focusedSeries,
});
let timeRange;
if (axesData && queryData?.queryName) {
const compositeQuery = (queryResponse?.data?.params as any)?.compositeQuery;
if (compositeQuery?.queries) {
const specificQuery = compositeQuery.queries.find(
(query: any) => query.spec?.name === queryData.queryName,
);
const stepInterval = specificQuery?.spec?.stepInterval || 60;
timeRange = getTimeRangeFromStepInterval(
stepInterval,
metric?.clickedTimestamp || xValue,
specificQuery?.spec?.signal === DataSource.METRICS &&
isApmMetric(specificQuery?.spec?.aggregations[0]?.metricName),
);
}
}
if (data && data?.record?.queryName) {
onClick(data.coord, { ...data.record, label: data.label, timeRange });
}
},
[onClick, queryResponse],
);
return {
coordinates,
popoverPosition,
onClose,
menuItemsConfig,
clickHandlerWithContextMenu,
};
};

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,76 +0,0 @@
import { useMemo } from 'react';
import cx from 'classnames';
import { calculateChartDimensions } from 'container/DashboardContainer/visualization/charts/utils';
import { LegendConfig, LegendPosition } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import './ChartLayout.styles.scss';
export interface ChartLayoutProps {
legendComponent: (legendPerSet: number) => React.ReactNode;
children: (props: {
chartWidth: number;
chartHeight: number;
averageLegendWidth: number;
}) => React.ReactNode;
layoutChildren?: React.ReactNode;
containerWidth: number;
containerHeight: number;
legendConfig: LegendConfig;
config: UPlotConfigBuilder;
}
export default function ChartLayout({
legendComponent,
children,
layoutChildren,
containerWidth,
containerHeight,
legendConfig,
config,
}: ChartLayoutProps): JSX.Element {
const chartDimensions = useMemo(
() => {
const legendItemsMap = config.getLegendItems();
const seriesLabels = Object.values(legendItemsMap)
.map((item) => item.label)
.filter((label): label is string => label !== undefined);
return calculateChartDimensions({
containerWidth,
containerHeight,
legendConfig,
seriesLabels,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[containerWidth, containerHeight, legendConfig],
);
return (
<div className="chart-layout__container">
<div
className={cx('chart-layout', {
'chart-layout--legend-right':
legendConfig.position === LegendPosition.RIGHT,
})}
>
<div className="chart-layout__content">
{children({
chartWidth: chartDimensions.width,
chartHeight: chartDimensions.height,
averageLegendWidth: chartDimensions.averageLegendWidth,
})}
</div>
<div
className="chart-layout__legend-wrapper"
style={{
height: chartDimensions.legendHeight,
width: chartDimensions.legendWidth,
}}
>
{legendComponent(chartDimensions.averageLegendWidth)}
</div>
</div>
{layoutChildren}
</div>
);
}

View File

@@ -1,4 +0,0 @@
.panel-container {
height: 100%;
width: 100%;
}

View File

@@ -1,176 +0,0 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import ChartManager from 'container/DashboardContainer/visualization/components/ChartManager/ChartManager';
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { LineInterpolation } from 'lib/uPlotV2/config/types';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
import '../Panel.styles.scss';
function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
const {
panelMode,
queryResponse,
widget,
onDragSelect,
isFullViewMode,
onToggleModelHandler,
} = props;
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [queryResponse]);
const {
coordinates,
popoverPosition,
onClose,
menuItemsConfig,
clickHandlerWithContextMenu,
} = usePanelContextMenu({
widget,
queryResponse,
});
const chartData = useMemo(() => {
if (!queryResponse?.data?.payload) {
return [];
}
return prepareChartData(queryResponse?.data?.payload);
}, [queryResponse?.data?.payload]);
const config = useMemo(() => {
const tzDate = (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value);
return prepareUPlotConfig({
widgetId: widget.id || '',
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
tzDate,
minTimeScale: minTimeScale,
maxTimeScale: maxTimeScale,
isLogScale: widget?.isLogScale ?? false,
thresholds: {
scaleKey: 'y',
thresholds: (widget.thresholds || []).map((threshold) => ({
thresholdValue: threshold.thresholdValue ?? 0,
thresholdColor: threshold.thresholdColor,
thresholdUnit: threshold.thresholdUnit,
thresholdLabel: threshold.thresholdLabel,
})),
yAxisUnit: widget.yAxisUnit,
},
yAxisUnit: widget.yAxisUnit || '',
softMin: widget.softMin === undefined ? null : widget.softMin,
softMax: widget.softMax === undefined ? null : widget.softMax,
spanGaps: false,
colorMapping: widget.customLegendColors ?? {},
lineInterpolation: LineInterpolation.Spline,
isDarkMode,
onClick: clickHandlerWithContextMenu,
onDragSelect,
currentQuery: widget.query,
panelMode,
});
}, [
widget.id,
maxTimeScale,
minTimeScale,
timezone.value,
widget.customLegendColors,
widget.isLogScale,
widget.softMax,
widget.softMin,
isDarkMode,
queryResponse?.data?.payload,
widget.query,
widget.thresholds,
widget.yAxisUnit,
panelMode,
clickHandlerWithContextMenu,
onDragSelect,
]);
const layoutChildren = useMemo(() => {
if (!isFullViewMode) {
return null;
}
return (
<ChartManager
config={config}
alignedData={chartData}
yAxisUnit={widget.yAxisUnit}
onCancel={onToggleModelHandler}
/>
);
}, [
isFullViewMode,
config,
chartData,
widget.yAxisUnit,
onToggleModelHandler,
]);
return (
<div className="panel-container" ref={graphRef}>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
<TimeSeries
config={config}
legendConfig={{
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
}}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}
>
<ContextMenu
coordinates={coordinates}
popoverPosition={popoverPosition}
title={menuItemsConfig.header as string}
items={menuItemsConfig.items}
onClose={onClose}
/>
</TimeSeries>
)}
</div>
);
}
export default TimeSeriesPanel;

View File

@@ -1,170 +0,0 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
fillMissingXAxisTimestamps,
getXAxisTimestamps,
} from 'container/DashboardContainer/visualization/panels/utils';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import onClickPlugin, {
OnClickPluginOpts,
} from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DistributionType,
DrawStyle,
LineInterpolation,
LineStyle,
SelectionPreferencesSource,
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PanelMode } from '../types';
export const prepareChartData = (
apiResponse: MetricRangePayloadProps,
): uPlot.AlignedData => {
const seriesList = apiResponse?.data?.result || [];
const timestampArr = getXAxisTimestamps(seriesList);
const yAxisValuesArr = fillMissingXAxisTimestamps(timestampArr, seriesList);
return [timestampArr, ...yAxisValuesArr];
};
export const prepareUPlotConfig = ({
widgetId,
apiResponse,
tzDate,
minTimeScale,
maxTimeScale,
isLogScale,
thresholds,
softMin,
softMax,
spanGaps,
colorMapping,
lineInterpolation,
isDarkMode,
currentQuery,
onDragSelect,
onClick,
yAxisUnit,
panelMode,
}: {
widgetId: string;
apiResponse: MetricRangePayloadProps;
tzDate: uPlot.LocalDateFromUnix;
minTimeScale: number | undefined;
maxTimeScale: number | undefined;
isLogScale: boolean;
softMin: number | null;
softMax: number | null;
spanGaps: boolean;
colorMapping: Record<string, string>;
lineInterpolation: LineInterpolation;
isDarkMode: boolean;
thresholds: ThresholdsDrawHookOptions;
currentQuery: Query;
yAxisUnit: string;
onDragSelect: (startTime: number, endTime: number) => void;
onClick?: OnClickPluginOpts['onClick'];
panelMode: PanelMode;
}): UPlotConfigBuilder => {
const builder = new UPlotConfigBuilder({
onDragSelect,
widgetId,
tzDate,
shouldSaveSelectionPreference: panelMode === PanelMode.DASHBOARD_VIEW,
selectionPreferencesSource: [
PanelMode.DASHBOARD_VIEW,
PanelMode.STANDALONE_VIEW,
].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY,
});
// X scale time axis
builder.addScale({
scaleKey: 'x',
time: true,
min: minTimeScale,
max: maxTimeScale,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
// Y scale value axis, driven primarily by softMin/softMax and data
builder.addScale({
scaleKey: 'y',
time: false,
min: undefined,
max: undefined,
softMin: softMin ?? undefined,
softMax: softMax ?? undefined,
thresholds,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
builder.addThresholds(thresholds);
if (typeof onClick === 'function') {
builder.addPlugin(
onClickPlugin({
onClick,
apiResponse,
}),
);
}
builder.addAxis({
scaleKey: 'x',
show: true,
side: 2,
isDarkMode,
isLogScale: false,
panelType: PANEL_TYPES.TIME_SERIES,
});
builder.addAxis({
scaleKey: 'y',
show: true,
side: 3,
isDarkMode,
isLogScale: false,
yAxisUnit,
panelType: PANEL_TYPES.TIME_SERIES,
});
apiResponse.data?.result?.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query
series.legend || '',
);
const label = currentQuery
? getLegend(series, currentQuery, baseLabelName)
: baseLabelName;
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Line,
label: label,
colorMapping,
spanGaps,
lineStyle: LineStyle.Solid,
lineInterpolation,
showPoints: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
});
});
return builder;
};

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,54 +0,0 @@
import { normalizePlotValue } from 'lib/uPlotV2/utils/dataUtils';
import { QueryData } from 'types/api/widgets/getQuery';
export function getXAxisTimestamps(seriesList: QueryData[]): number[] {
const timestamps = new Set<number>();
seriesList.forEach((series: { values?: [number, string][] }) => {
if (series?.values) {
series.values.forEach((value) => {
timestamps.add(value[0]);
});
}
});
const timestampsArr = Array.from(timestamps);
timestampsArr.sort((a, b) => a - b);
return timestampsArr;
}
export function fillMissingXAxisTimestamps(
timestampArr: number[],
data: Array<{ values?: [number, string][] }>,
): (number | null)[][] {
// Ensure we work with a sorted, deduplicated list of x-axis timestamps
const canonicalTimestamps = Array.from(new Set(timestampArr)).sort(
(a, b) => a - b,
);
return data.map(({ values }) =>
buildSeriesYValues(canonicalTimestamps, values),
);
}
function buildSeriesYValues(
timestamps: number[],
values?: [number, string][],
): (number | null)[] {
if (!values?.length) {
return [];
}
const valueByTimestamp = new Map<number, number | null>();
for (let i = 0; i < values.length; i++) {
const [timestamp, rawValue] = values[i];
valueByTimestamp.set(timestamp, normalizePlotValue(rawValue));
}
return timestamps.map((timestamp) => {
const value = valueByTimestamp.get(timestamp);
return value !== undefined ? value : null;
});
}

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

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

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

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