Compare commits

..

24 Commits

Author SHA1 Message Date
Srikanth Chekuri
aea42ba5ca Merge branch 'main' into enhancement/cmd-click-stack 2025-10-30 00:33:25 +05:30
Manika Malhotra
455ba0549f Merge branch 'main' into enhancement/cmd-click-stack 2025-10-22 11:32:29 +05:30
manika-signoz
5f2c302551 chore: extract isEventObject utility to separate file 2025-10-13 23:56:49 +05:30
manika-signoz
15c2dc700a chore: use requireActual 2025-10-13 23:41:21 +05:30
manika-signoz
02fa0dbc32 Merge branch 'main' into enhancement/cmd-click-stack 2025-10-13 23:25:35 +05:30
manika-signoz
e0948033c8 chore: re-export isEventObject utility from mocks 2025-10-13 23:24:28 +05:30
manika-signoz
a1115ac65b Merge branch 'main' into enhancement/cmd-click-stack 2025-10-13 10:30:04 +05:30
manika-signoz
9bcb88c747 Merge branch 'main' into enhancement/cmd-click-stack 2025-10-09 17:03:12 +05:30
manika-signoz
367bf7b4b5 Merge branch 'main' into enhancement/cmd-click-stack 2025-10-09 00:57:31 +05:30
manika-signoz
59b68057b8 Merge branch 'main' into enhancement/cmd-click-stack 2025-10-08 15:38:11 +05:30
manika-signoz
fa1b2ddf7c Merge branch 'main' into enhancement/cmd-click-stack 2025-10-07 23:07:15 +05:30
manika-signoz
642a0e5656 fix: dashboardandalertspopover.test to use usesafenav mock 2025-09-30 19:31:34 +05:30
manika-signoz
cb99ee1ac1 fix: failing test cases due to isEventObject, add to mock 2025-09-30 19:16:54 +05:30
manika-signoz
7616cb89e4 Merge branch 'enhancement/cmd-click-stack' of github.com:SigNoz/signoz into enhancement/cmd-click-stack 2025-09-30 17:41:07 +05:30
manika-signoz
bf780c7445 chore: resolve comments, improve type safety in usesafenav 2025-09-30 17:40:17 +05:30
manika-signoz
61062dfd8d Merge branch 'main' into enhancement/cmd-click-stack 2025-09-30 17:08:38 +05:30
manika-signoz
5b7af9651c Merge branch 'main' into enhancement/cmd-click-stack 2025-09-29 10:40:22 +05:30
manika-signoz
b9012f6150 Merge branch 'main' into enhancement/cmd-click-stack 2025-09-24 13:12:36 +05:30
manika-signoz
7ab81780b3 Merge branch 'main' into enhancement/cmd-click-stack 2025-09-24 10:06:51 +05:30
manika-signoz
a16f51457f Merge branch 'main' into enhancement/cmd-click-stack 2025-09-23 16:32:51 +05:30
manika-signoz
38a38b5645 test: add tests for history.push 2025-09-23 16:30:16 +05:30
manika-signoz
bb04bc5044 Merge branch 'main' into enhancement/cmd-click-stack 2025-09-23 16:16:16 +05:30
manika-signoz
58736f40dc feat: add support for location object in history.push override 2025-09-22 19:05:42 +05:30
manika-signoz
91154249d6 feat: add history.push and safeNavigate method overrides 2025-09-22 18:55:52 +05:30
65 changed files with 1001 additions and 2799 deletions

2
.github/CODEOWNERS vendored
View File

@@ -2,7 +2,7 @@
# Owners are automatically requested for review for PRs that changes code
# that they own.
/frontend/ @YounixM @aks07
/frontend/ @SigNoz/frontend @YounixM
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv

1
.gitignore vendored
View File

@@ -106,6 +106,7 @@ downloads/
eggs/
.eggs/
lib/
!frontend/src/lib/
lib64/
parts/
sdist/

View File

@@ -1,63 +1,43 @@
version: "2"
linters:
default: none
default: standard
enable:
- bodyclose
- depguard
- errcheck
- forbidigo
- govet
- iface
- ineffassign
- misspell
- nilnil
- sloglint
- depguard
- iface
- unparam
- unused
settings:
depguard:
rules:
noerrors:
deny:
- pkg: errors
desc: Do not use errors package. Use github.com/SigNoz/signoz/pkg/errors instead.
nozap:
deny:
- pkg: go.uber.org/zap
desc: Do not use zap logger. Use slog instead.
forbidigo:
forbid:
- pattern: fmt.Errorf
- pattern: ^(fmt\.Print.*|print|println)$
iface:
enable:
- identical
sloglint:
no-mixed-args: true
kv-only: true
no-global: all
context: all
static-msg: true
key-naming-case: snake
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- pkg/query-service
- ee/query-service
- scripts/
- tmp/
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
- forbidigo
linters-settings:
sloglint:
no-mixed-args: true
kv-only: true
no-global: all
context: all
static-msg: true
msg-style: lowercased
key-naming-case: snake
depguard:
rules:
nozap:
deny:
- pkg: "go.uber.org/zap"
desc: "Do not use zap logger. Use slog instead."
noerrors:
deny:
- pkg: "errors"
desc: "Do not use errors package. Use github.com/SigNoz/signoz/pkg/errors instead."
iface:
enable:
- identical
forbidigo:
forbid:
- fmt.Errorf
- ^(fmt\.Print.*|print|println)$
issues:
exclude-dirs:
- "pkg/query-service"
- "ee/query-service"
- "scripts/"

View File

@@ -1,157 +0,0 @@
package postgressqlstore
import (
"strings"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun/schema"
)
type formatter struct {
bunf schema.Formatter
}
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
return &formatter{bunf: schema.NewFormatter(dialect)}
}
func (f *formatter) JSONExtractString(column, path string) []byte {
var sql []byte
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, f.convertJSONPathToPostgres(path)...)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "jsonb_typeof("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONIsArray(column, path string) []byte {
var sql []byte
sql = append(sql, f.JSONType(column, path)...)
sql = append(sql, " = 'array'"...)
return sql
}
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "jsonb_array_elements("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias)
}
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "jsonb_array_elements_text("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + "::text")
}
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "jsonb_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, f.convertJSONPathToPostgresWithMode(path, false)...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".key")
}
func (f *formatter) JSONArrayAgg(expression string) []byte {
var sql []byte
sql = append(sql, "jsonb_agg("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
if len(values) == 0 {
return []byte("jsonb_build_array()")
}
var sql []byte
sql = append(sql, "jsonb_build_array("...)
for i, v := range values {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, '\'')
sql = append(sql, v...)
sql = append(sql, '\'')
}
sql = append(sql, ')')
return sql
}
func (f *formatter) TextToJsonColumn(column string) []byte {
var sql []byte
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, "::jsonb"...)
return sql
}
func (f *formatter) convertJSONPathToPostgres(jsonPath string) string {
return f.convertJSONPathToPostgresWithMode(jsonPath, true)
}
func (f *formatter) convertJSONPathToPostgresWithMode(jsonPath string, asText bool) string {
path := strings.TrimPrefix(jsonPath, "$")
if path == "" || path == "." {
return ""
}
parts := strings.Split(strings.TrimPrefix(path, "."), ".")
if len(parts) == 0 {
return ""
}
var result strings.Builder
for i, part := range parts {
if i < len(parts)-1 {
result.WriteString("->")
result.WriteString("'")
result.WriteString(part)
result.WriteString("'")
} else {
if asText {
result.WriteString("->>")
} else {
result.WriteString("->")
}
result.WriteString("'")
result.WriteString(part)
result.WriteString("'")
}
}
return result.String()
}
func (f *formatter) LowerExpression(expression string) []byte {
var sql []byte
sql = append(sql, "lower("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}

View File

@@ -1,488 +0,0 @@
package postgressqlstore
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/uptrace/bun/dialect/pgdialect"
)
func TestJSONExtractString(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `"data"->>'field'`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.name",
expected: `"metadata"->'user'->>'name'`,
},
{
name: "deeply nested path",
column: "json_col",
path: "$.level1.level2.level3",
expected: `"json_col"->'level1'->'level2'->>'level3'`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `"json_col"`,
},
{
name: "empty path",
column: "data",
path: "",
expected: `"data"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONExtractString(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONType(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `jsonb_typeof("data"->'field')`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.age",
expected: `jsonb_typeof("metadata"->'user'->'age')`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `jsonb_typeof("json_col")`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONType(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONIsArray(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.items",
expected: `jsonb_typeof("data"->'items') = 'array'`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.tags",
expected: `jsonb_typeof("metadata"->'user'->'tags') = 'array'`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `jsonb_typeof("json_col") = 'array'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONIsArray(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayElements(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "elem",
expected: `jsonb_array_elements("data") AS "elem"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "elem",
expected: `jsonb_array_elements("data") AS "elem"`,
},
{
name: "nested path",
column: "metadata",
path: "$.items",
alias: "item",
expected: `jsonb_array_elements("metadata"->'items') AS "item"`,
},
{
name: "deeply nested path",
column: "json_col",
path: "$.user.tags",
alias: "tag",
expected: `jsonb_array_elements("json_col"->'user'->'tags') AS "tag"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got, _ := f.JSONArrayElements(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayOfStrings(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "str",
expected: `jsonb_array_elements_text("data") AS "str"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "str",
expected: `jsonb_array_elements_text("data") AS "str"`,
},
{
name: "nested path",
column: "metadata",
path: "$.strings",
alias: "s",
expected: `jsonb_array_elements_text("metadata"->'strings') AS "s"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got, _ := f.JSONArrayOfStrings(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONKeys(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "k",
expected: `jsonb_each("data") AS "k"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "k",
expected: `jsonb_each("data") AS "k"`,
},
{
name: "nested path",
column: "metadata",
path: "$.object",
alias: "key",
expected: `jsonb_each("metadata"->'object') AS "key"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got, _ := f.JSONKeys(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayAgg(t *testing.T) {
tests := []struct {
name string
expression string
expected string
}{
{
name: "simple column",
expression: "id",
expected: "jsonb_agg(id)",
},
{
name: "expression with function",
expression: "DISTINCT name",
expected: "jsonb_agg(DISTINCT name)",
},
{
name: "complex expression",
expression: "data->>'field'",
expected: "jsonb_agg(data->>'field')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONArrayAgg(tt.expression))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayLiteral(t *testing.T) {
tests := []struct {
name string
values []string
expected string
}{
{
name: "empty array",
values: []string{},
expected: "jsonb_build_array()",
},
{
name: "single value",
values: []string{"value1"},
expected: "jsonb_build_array('value1')",
},
{
name: "multiple values",
values: []string{"value1", "value2", "value3"},
expected: "jsonb_build_array('value1', 'value2', 'value3')",
},
{
name: "values with special characters",
values: []string{"test", "with space", "with-dash"},
expected: "jsonb_build_array('test', 'with space', 'with-dash')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.JSONArrayLiteral(tt.values...))
assert.Equal(t, tt.expected, got)
})
}
}
func TestConvertJSONPathToPostgresWithMode(t *testing.T) {
tests := []struct {
name string
jsonPath string
asText bool
expected string
}{
{
name: "simple path as text",
jsonPath: "$.field",
asText: true,
expected: "->>'field'",
},
{
name: "simple path as json",
jsonPath: "$.field",
asText: false,
expected: "->'field'",
},
{
name: "nested path as text",
jsonPath: "$.user.name",
asText: true,
expected: "->'user'->>'name'",
},
{
name: "nested path as json",
jsonPath: "$.user.name",
asText: false,
expected: "->'user'->'name'",
},
{
name: "deeply nested as text",
jsonPath: "$.a.b.c.d",
asText: true,
expected: "->'a'->'b'->'c'->>'d'",
},
{
name: "root path",
jsonPath: "$",
asText: true,
expected: "",
},
{
name: "empty path",
jsonPath: "",
asText: true,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New()).(*formatter)
got := f.convertJSONPathToPostgresWithMode(tt.jsonPath, tt.asText)
assert.Equal(t, tt.expected, got)
})
}
}
func TestTextToJsonColumn(t *testing.T) {
tests := []struct {
name string
column string
expected string
}{
{
name: "simple column name",
column: "data",
expected: `"data"::jsonb`,
},
{
name: "column with underscore",
column: "user_data",
expected: `"user_data"::jsonb`,
},
{
name: "column with special characters",
column: "json-col",
expected: `"json-col"::jsonb`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.TextToJsonColumn(tt.column))
assert.Equal(t, tt.expected, got)
})
}
}
func TestLowerExpression(t *testing.T) {
tests := []struct {
name string
expr string
expected string
}{
{
name: "simple column name",
expr: "name",
expected: "lower(name)",
},
{
name: "quoted column identifier",
expr: `"column_name"`,
expected: `lower("column_name")`,
},
{
name: "jsonb text extraction",
expr: "data->>'field'",
expected: "lower(data->>'field')",
},
{
name: "nested jsonb extraction",
expr: "metadata->'user'->>'name'",
expected: "lower(metadata->'user'->>'name')",
},
{
name: "jsonb_typeof expression",
expr: "jsonb_typeof(data->'field')",
expected: "lower(jsonb_typeof(data->'field'))",
},
{
name: "string concatenation",
expr: "first_name || ' ' || last_name",
expected: "lower(first_name || ' ' || last_name)",
},
{
name: "CAST expression",
expr: "CAST(value AS TEXT)",
expected: "lower(CAST(value AS TEXT))",
},
{
name: "COALESCE expression",
expr: "COALESCE(name, 'default')",
expected: "lower(COALESCE(name, 'default'))",
},
{
name: "subquery column",
expr: "users.email",
expected: "lower(users.email)",
},
{
name: "quoted identifier with special chars",
expr: `"user-name"`,
expected: `lower("user-name")`,
},
{
name: "jsonb to text cast",
expr: "data::text",
expected: "lower(data::text)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(pgdialect.New())
got := string(f.LowerExpression(tt.expr))
assert.Equal(t, tt.expected, got)
})
}
}

View File

@@ -3,6 +3,7 @@ package postgressqlstore
import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -14,11 +15,10 @@ import (
)
type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
formatter sqlstore.SQLFormatter
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
}
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
@@ -55,14 +55,11 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
sqldb := stdlib.OpenDBFromPool(pool)
pgDialect := pgdialect.New()
bunDB := sqlstore.NewBunDB(settings, sqldb, pgDialect, hooks)
return &provider{
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgDialect, hooks),
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
dialect: new(dialect),
}, nil
}
@@ -78,10 +75,6 @@ func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
func (provider *provider) Formatter() sqlstore.SQLFormatter {
return provider.formatter
}
func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB {
return provider.bundb.BunDBCtx(ctx)
}

View File

@@ -1,4 +1,6 @@
// Mock for useSafeNavigate hook to avoid React Router version conflicts in tests
export { isEventObject } from '../src/utils/isEventObject';
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;

View File

@@ -69,7 +69,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"axios": "1.8.2",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",

View File

@@ -1,371 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { getYAxisFormattedValue, PrecisionOptionsEnum } from '../yAxisConfig';
const testFullPrecisionGetYAxisFormattedValue = (
value: string,
format: string,
): string => getYAxisFormattedValue(value, format, PrecisionOptionsEnum.FULL);
describe('getYAxisFormattedValue - none (full precision legacy assertions)', () => {
test('large integers and decimals', () => {
expect(testFullPrecisionGetYAxisFormattedValue('250034', 'none')).toBe(
'250034',
);
expect(
testFullPrecisionGetYAxisFormattedValue('250034897.12345', 'none'),
).toBe('250034897.12345');
expect(
testFullPrecisionGetYAxisFormattedValue('250034897.02354', 'none'),
).toBe('250034897.02354');
expect(testFullPrecisionGetYAxisFormattedValue('9999999.9999', 'none')).toBe(
'9999999.9999',
);
});
test('preserves leading zeros after decimal until first non-zero', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1.0000234', 'none')).toBe(
'1.0000234',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.00003', 'none')).toBe(
'0.00003',
);
});
test('trims to three significant decimals and removes trailing zeros', () => {
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250034', 'none'),
).toBe('0.000000250034');
expect(testFullPrecisionGetYAxisFormattedValue('0.00000025', 'none')).toBe(
'0.00000025',
);
// Big precision, limiting the javascript precision (~16 digits)
expect(
testFullPrecisionGetYAxisFormattedValue('1.0000000000000001', 'none'),
).toBe('1');
expect(
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'none'),
).toBe('1.005555555595958');
expect(testFullPrecisionGetYAxisFormattedValue('0.000000001', 'none')).toBe(
'0.000000001',
);
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250000', 'none'),
).toBe('0.00000025');
});
test('whole numbers normalize', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'none')).toBe('1000');
expect(testFullPrecisionGetYAxisFormattedValue('99.5458', 'none')).toBe(
'99.5458',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.234567', 'none')).toBe(
'1.234567',
);
expect(testFullPrecisionGetYAxisFormattedValue('99.998', 'none')).toBe(
'99.998',
);
});
test('strip redundant decimal zeros', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000.000', 'none')).toBe(
'1000',
);
expect(testFullPrecisionGetYAxisFormattedValue('99.500', 'none')).toBe(
'99.5',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.000', 'none')).toBe('1');
});
test('edge values', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('-0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'none')).toBe('∞');
expect(testFullPrecisionGetYAxisFormattedValue('-Infinity', 'none')).toBe(
'-∞',
);
expect(testFullPrecisionGetYAxisFormattedValue('invalid', 'none')).toBe(
'NaN',
);
expect(testFullPrecisionGetYAxisFormattedValue('', 'none')).toBe('NaN');
expect(testFullPrecisionGetYAxisFormattedValue('abc123', 'none')).toBe('NaN');
});
test('small decimals keep precision as-is', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.0001', 'none')).toBe(
'0.0001',
);
expect(testFullPrecisionGetYAxisFormattedValue('-0.0001', 'none')).toBe(
'-0.0001',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.000000001', 'none')).toBe(
'0.000000001',
);
});
test('simple decimals preserved', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.1', 'none')).toBe('0.1');
expect(testFullPrecisionGetYAxisFormattedValue('0.2', 'none')).toBe('0.2');
expect(testFullPrecisionGetYAxisFormattedValue('0.3', 'none')).toBe('0.3');
expect(testFullPrecisionGetYAxisFormattedValue('1.0000000001', 'none')).toBe(
'1.0000000001',
);
});
});
describe('getYAxisFormattedValue - units (full precision legacy assertions)', () => {
test('ms', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'ms')).toBe('1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('500', 'ms')).toBe('500 ms');
expect(testFullPrecisionGetYAxisFormattedValue('60000', 'ms')).toBe('1 min');
expect(testFullPrecisionGetYAxisFormattedValue('295.429', 'ms')).toBe(
'295.429 ms',
);
expect(testFullPrecisionGetYAxisFormattedValue('4353.81', 'ms')).toBe(
'4.35381 s',
);
});
test('s', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 's')).toBe('1.5 mins');
expect(testFullPrecisionGetYAxisFormattedValue('30', 's')).toBe('30 s');
expect(testFullPrecisionGetYAxisFormattedValue('3600', 's')).toBe('1 hour');
});
test('m', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 'm')).toBe('1.5 hours');
expect(testFullPrecisionGetYAxisFormattedValue('30', 'm')).toBe('30 min');
expect(testFullPrecisionGetYAxisFormattedValue('1440', 'm')).toBe('1 day');
});
test('bytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'bytes')).toBe(
'1 KiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('512', 'bytes')).toBe('512 B');
expect(testFullPrecisionGetYAxisFormattedValue('1536', 'bytes')).toBe(
'1.5 KiB',
);
});
test('mbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'mbytes')).toBe(
'1 GiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('512', 'mbytes')).toBe(
'512 MiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('1536', 'mbytes')).toBe(
'1.5 GiB',
);
});
test('kbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'kbytes')).toBe(
'1 MiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('512', 'kbytes')).toBe(
'512 KiB',
);
expect(testFullPrecisionGetYAxisFormattedValue('1536', 'kbytes')).toBe(
'1.5 MiB',
);
});
test('short', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'short')).toBe('1 K');
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'short')).toBe(
'1.5 K',
);
expect(testFullPrecisionGetYAxisFormattedValue('999', 'short')).toBe('999');
expect(testFullPrecisionGetYAxisFormattedValue('1000000', 'short')).toBe(
'1 Mil',
);
expect(testFullPrecisionGetYAxisFormattedValue('1555600', 'short')).toBe(
'1.5556 Mil',
);
expect(testFullPrecisionGetYAxisFormattedValue('999999', 'short')).toBe(
'999.999 K',
);
expect(testFullPrecisionGetYAxisFormattedValue('1000000000', 'short')).toBe(
'1 Bil',
);
expect(testFullPrecisionGetYAxisFormattedValue('1500000000', 'short')).toBe(
'1.5 Bil',
);
expect(testFullPrecisionGetYAxisFormattedValue('999999999', 'short')).toBe(
'999.999999 Mil',
);
});
test('percent', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.15', 'percent')).toBe(
'0.15%',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.1234', 'percent')).toBe(
'0.1234%',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.123499', 'percent')).toBe(
'0.123499%',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.5', 'percent')).toBe(
'1.5%',
);
expect(testFullPrecisionGetYAxisFormattedValue('0.0001', 'percent')).toBe(
'0.0001%',
);
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000001', 'percent'),
).toBe('1e-9%');
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250034', 'percent'),
).toBe('0.000000250034%');
expect(testFullPrecisionGetYAxisFormattedValue('0.00000025', 'percent')).toBe(
'0.00000025%',
);
// Big precision, limiting the javascript precision (~16 digits)
expect(
testFullPrecisionGetYAxisFormattedValue('1.0000000000000001', 'percent'),
).toBe('1%');
expect(
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
).toBe('1.005555555595958%');
});
test('ratio', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.5', 'ratio')).toBe(
'0.5 ratio',
);
expect(testFullPrecisionGetYAxisFormattedValue('1.25', 'ratio')).toBe(
'1.25 ratio',
);
expect(testFullPrecisionGetYAxisFormattedValue('2.0', 'ratio')).toBe(
'2 ratio',
);
});
test('temperature units', () => {
expect(testFullPrecisionGetYAxisFormattedValue('25', 'celsius')).toBe(
'25 °C',
);
expect(testFullPrecisionGetYAxisFormattedValue('0', 'celsius')).toBe('0 °C');
expect(testFullPrecisionGetYAxisFormattedValue('-10', 'celsius')).toBe(
'-10 °C',
);
expect(testFullPrecisionGetYAxisFormattedValue('77', 'fahrenheit')).toBe(
'77 °F',
);
expect(testFullPrecisionGetYAxisFormattedValue('32', 'fahrenheit')).toBe(
'32 °F',
);
expect(testFullPrecisionGetYAxisFormattedValue('14', 'fahrenheit')).toBe(
'14 °F',
);
});
test('ms edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'ms')).toBe('0 ms');
expect(testFullPrecisionGetYAxisFormattedValue('-1500', 'ms')).toBe('-1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'ms')).toBe('∞');
});
test('bytes edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'bytes')).toBe('0 B');
expect(testFullPrecisionGetYAxisFormattedValue('-1024', 'bytes')).toBe(
'-1 KiB',
);
});
});
describe('getYAxisFormattedValue - precision option tests', () => {
test('precision 0 drops decimal part', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 0)).toBe('1');
expect(getYAxisFormattedValue('0.9999', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('12345.6789', 'none', 0)).toBe('12345');
expect(getYAxisFormattedValue('0.0000123456', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('1000.000', 'none', 0)).toBe('1000');
expect(getYAxisFormattedValue('0.000000250034', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 0)).toBe('1');
// with unit
expect(getYAxisFormattedValue('4353.81', 'ms', 0)).toBe('4 s');
});
test('precision 1,2,3,4 decimals', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 1)).toBe('1.2');
expect(getYAxisFormattedValue('1.2345', 'none', 2)).toBe('1.23');
expect(getYAxisFormattedValue('1.2345', 'none', 3)).toBe('1.234');
expect(getYAxisFormattedValue('1.2345', 'none', 4)).toBe('1.2345');
expect(getYAxisFormattedValue('0.0000123456', 'none', 1)).toBe('0.00001');
expect(getYAxisFormattedValue('0.0000123456', 'none', 2)).toBe('0.000012');
expect(getYAxisFormattedValue('0.0000123456', 'none', 3)).toBe('0.0000123');
expect(getYAxisFormattedValue('0.0000123456', 'none', 4)).toBe('0.00001234');
expect(getYAxisFormattedValue('1000.000', 'none', 1)).toBe('1000');
expect(getYAxisFormattedValue('1000.000', 'none', 2)).toBe('1000');
expect(getYAxisFormattedValue('1000.000', 'none', 3)).toBe('1000');
expect(getYAxisFormattedValue('1000.000', 'none', 4)).toBe('1000');
expect(getYAxisFormattedValue('0.000000250034', 'none', 1)).toBe('0.0000002');
expect(getYAxisFormattedValue('0.000000250034', 'none', 2)).toBe(
'0.00000025',
); // leading zeros + 2 significant => same trimmed
expect(getYAxisFormattedValue('0.000000250034', 'none', 3)).toBe(
'0.00000025',
);
expect(getYAxisFormattedValue('0.000000250304', 'none', 4)).toBe(
'0.0000002503',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 1)).toBe(
'1.005',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 2)).toBe(
'1.0055',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 3)).toBe(
'1.00555',
);
expect(getYAxisFormattedValue('1.00555555559595876', 'none', 4)).toBe(
'1.005555',
);
// with unit
expect(getYAxisFormattedValue('4353.81', 'ms', 1)).toBe('4.4 s');
expect(getYAxisFormattedValue('4353.81', 'ms', 2)).toBe('4.35 s');
expect(getYAxisFormattedValue('4353.81', 'ms', 3)).toBe('4.354 s');
expect(getYAxisFormattedValue('4353.81', 'ms', 4)).toBe('4.3538 s');
// Percentages
expect(getYAxisFormattedValue('0.123456', 'percent', 2)).toBe('0.12%');
expect(getYAxisFormattedValue('0.123456', 'percent', 4)).toBe('0.1235%'); // approximation
});
test('precision full uses up to DEFAULT_SIGNIFICANT_DIGITS significant digits', () => {
expect(
getYAxisFormattedValue(
'0.00002625429914148441',
'none',
PrecisionOptionsEnum.FULL,
),
).toBe('0.000026254299141');
expect(
getYAxisFormattedValue(
'0.000026254299141484417',
's',
PrecisionOptionsEnum.FULL,
),
).toBe('26254299141484417000000 µs');
expect(
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),
).toBe('4.35381 s');
expect(getYAxisFormattedValue('500', 'ms', PrecisionOptionsEnum.FULL)).toBe(
'500 ms',
);
});
});

View File

@@ -1,158 +1,58 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { formattedValueToString, getValueFormat } from '@grafana/data';
import * as Sentry from '@sentry/react';
import { isNaN } from 'lodash-es';
const DEFAULT_SIGNIFICANT_DIGITS = 15;
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
const MAX_DECIMALS = 15;
export enum PrecisionOptionsEnum {
ZERO = 0,
ONE = 1,
TWO = 2,
THREE = 3,
FOUR = 4,
FULL = 'full',
}
export type PrecisionOption = 0 | 1 | 2 | 3 | 4 | PrecisionOptionsEnum.FULL;
/**
* Formats a number for display, preserving leading zeros after the decimal point
* and showing up to DEFAULT_SIGNIFICANT_DIGITS digits after the first non-zero decimal digit.
* It avoids scientific notation and removes unnecessary trailing zeros.
*
* @example
* formatDecimalWithLeadingZeros(1.2345); // "1.2345"
* formatDecimalWithLeadingZeros(0.0012345); // "0.0012345"
* formatDecimalWithLeadingZeros(5.0); // "5"
*
* @param value The number to format.
* @returns The formatted string.
*/
const formatDecimalWithLeadingZeros = (
value: number,
precision: PrecisionOption,
): string => {
if (value === 0) {
return '0';
}
// Use toLocaleString to get a full decimal representation without scientific notation.
const numStr = value.toLocaleString('en-US', {
useGrouping: false,
maximumFractionDigits: 20,
});
const [integerPart, decimalPart = ''] = numStr.split('.');
// If there's no decimal part, the integer part is the result.
if (!decimalPart) {
return integerPart;
}
// Find the index of the first non-zero digit in the decimal part.
const firstNonZeroIndex = decimalPart.search(/[^0]/);
// If the decimal part consists only of zeros, return just the integer part.
if (firstNonZeroIndex === -1) {
return integerPart;
}
// Determine the number of decimals to keep: leading zeros + up to N significant digits.
const significantDigits =
precision === PrecisionOptionsEnum.FULL
? DEFAULT_SIGNIFICANT_DIGITS
: precision;
const decimalsToKeep = firstNonZeroIndex + (significantDigits || 0);
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
const finalDecimalsToKeep = Math.min(decimalsToKeep, MAX_DECIMALS);
const trimmedDecimalPart = decimalPart.substring(0, finalDecimalsToKeep);
// If precision is 0, we drop the decimal part entirely.
if (precision === 0) {
return integerPart;
}
// Remove any trailing zeros from the result to keep it clean.
const finalDecimalPart = trimmedDecimalPart.replace(/0+$/, '');
// Return the integer part, or the integer and decimal parts combined.
return finalDecimalPart ? `${integerPart}.${finalDecimalPart}` : integerPart;
};
/**
* Formats a Y-axis value based on a given format string.
*
* @param value The string value from the axis.
* @param format The format identifier (e.g. 'none', 'ms', 'bytes', 'short').
* @returns A formatted string ready for display.
*/
export const getYAxisFormattedValue = (
value: string,
format: string,
precision: PrecisionOption = 2, // default precision requested
): string => {
const numValue = parseFloat(value);
// Handle non-numeric or special values first.
if (isNaN(numValue)) return 'NaN';
if (numValue === Infinity) return '∞';
if (numValue === -Infinity) return '-∞';
const decimalPlaces = value.split('.')[1]?.length || undefined;
// Use custom formatter for the 'none' format honoring precision
if (format === 'none') {
return formatDecimalWithLeadingZeros(numValue, precision);
}
// For all other standard formats, delegate to grafana/data's built-in formatter.
const computeDecimals = (): number | undefined => {
if (precision === PrecisionOptionsEnum.FULL) {
return decimalPlaces && decimalPlaces >= DEFAULT_SIGNIFICANT_DIGITS
? decimalPlaces
: DEFAULT_SIGNIFICANT_DIGITS;
}
return precision;
};
const fallbackFormat = (): string => {
if (precision === PrecisionOptionsEnum.FULL) return numValue.toString();
if (precision === 0) return Math.round(numValue).toString();
return precision !== undefined
? numValue
.toFixed(precision)
.replace(/(\.[0-9]*[1-9])0+$/, '$1') // trimming zeros
.replace(/\.$/, '')
: numValue.toString();
};
let decimalPrecision: number | undefined;
const parsedValue = getValueFormat(format)(
parseFloat(value),
undefined,
undefined,
undefined,
);
try {
const formatter = getValueFormat(format);
const formattedValue = formatter(numValue, computeDecimals(), undefined);
if (formattedValue.text && formattedValue.text.includes('.')) {
formattedValue.text = formatDecimalWithLeadingZeros(
parseFloat(formattedValue.text),
precision,
);
const decimalSplitted = parsedValue.text.split('.');
if (decimalSplitted.length === 1) {
decimalPrecision = 0;
} else {
const decimalDigits = decimalSplitted[1].split('');
decimalPrecision = decimalDigits.length;
let nonZeroCtr = 0;
for (let idx = 0; idx < decimalDigits.length; idx += 1) {
if (decimalDigits[idx] !== '0') {
nonZeroCtr += 1;
if (nonZeroCtr >= 2) {
decimalPrecision = idx + 1;
}
} else if (nonZeroCtr) {
decimalPrecision = idx;
break;
}
}
}
return formattedValueToString(formattedValue);
return formattedValueToString(
getValueFormat(format)(
parseFloat(value),
decimalPrecision,
undefined,
undefined,
),
);
} catch (error) {
Sentry.captureEvent({
message: `Error applying formatter: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
level: 'error',
});
return fallbackFormat();
console.error(error);
}
return `${parseFloat(value)}`;
};
export const getToolTipValue = (
value: string | number,
format?: string,
precision?: PrecisionOption,
): string =>
getYAxisFormattedValue(value?.toString(), format || 'none', precision);
export const getToolTipValue = (value: string, format?: string): string => {
try {
return formattedValueToString(
getValueFormat(format)(parseFloat(value), undefined, undefined, undefined),
);
} catch (error) {
console.error(error);
}
return `${value}`;
};

View File

@@ -60,14 +60,6 @@ function Metrics({
setElement,
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const queryPayloads = useMemo(
() =>
getHostQueryPayload(
@@ -155,13 +147,6 @@ function Metrics({
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
),
[

View File

@@ -69,13 +69,6 @@ function StatusCodeBarCharts({
} = endPointStatusCodeLatencyBarChartsDataQuery;
const { startTime: minTime, endTime: maxTime } = timeRange;
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
@@ -214,13 +207,6 @@ function StatusCodeBarCharts({
onDragSelect,
colorMapping,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
[
minTime,

View File

@@ -108,13 +108,6 @@ function ChartPreview({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
@@ -303,13 +296,6 @@ function ChartPreview({
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
[
yAxisUnit,

View File

@@ -48,7 +48,6 @@ function GridTableComponent({
widgetId,
panelType,
queryRangeRequest,
decimalPrecision,
...props
}: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
@@ -88,19 +87,10 @@ function GridTableComponent({
const newValue = { ...val };
Object.keys(val).forEach((k) => {
const unit = getColumnUnit(k, columnUnits);
// Apply formatting if:
// 1. Column has a unit defined, OR
// 2. decimalPrecision is specified (format all values)
const shouldFormat = unit || decimalPrecision !== undefined;
if (shouldFormat) {
if (unit) {
// the check below takes care of not adding units for rows that have n/a or null values
if (val[k] !== 'n/a' && val[k] !== null) {
newValue[k] = getYAxisFormattedValue(
String(val[k]),
unit || 'none',
decimalPrecision,
);
newValue[k] = getYAxisFormattedValue(String(val[k]), unit);
} else if (val[k] === null) {
newValue[k] = 'n/a';
}
@@ -113,7 +103,7 @@ function GridTableComponent({
return mutateDataSource;
},
[columnUnits, decimalPrecision],
[columnUnits],
);
const dataSource = useMemo(() => applyColumnUnits(originalDataSource), [

View File

@@ -1,5 +1,4 @@
import { TableProps } from 'antd';
import { PrecisionOption } from 'components/Graph/yAxisConfig';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
import {
@@ -16,7 +15,6 @@ export type GridTableComponentProps = {
query: Query;
thresholds?: ThresholdProps[];
columnUnits?: ColumnUnit;
decimalPrecision?: PrecisionOption;
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
sticky?: TableProps<RowData>['sticky'];
searchTerm?: string;

View File

@@ -99,11 +99,7 @@ function GridValueComponent({
rawValue={value}
value={
yAxisUnit
? getYAxisFormattedValue(
String(value),
yAxisUnit,
widget?.decimalPrecision,
)
? getYAxisFormattedValue(String(value), yAxisUnit)
: value.toString()
}
/>

View File

@@ -115,13 +115,6 @@ function EntityMetrics<T>({
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const { currentQuery } = useQueryBuilder();
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const chartData = useMemo(
() =>
@@ -191,13 +184,6 @@ function EntityMetrics<T>({
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
});
}),
[

View File

@@ -83,13 +83,6 @@ function NodeMetrics({
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
@@ -116,13 +109,6 @@ function NodeMetrics({
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
),
[

View File

@@ -45,14 +45,6 @@ function PodMetrics({
};
}, [logLineTimestamp]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
@@ -99,13 +91,6 @@ function PodMetrics({
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
query: currentQuery,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
}),
),
[

View File

@@ -23,11 +23,15 @@ const mockAlerts = [mockAlert1, mockAlert2];
const mockDashboards = [mockDashboard1, mockDashboard2];
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: mockSafeNavigate,
}),
}));
jest.mock('hooks/useSafeNavigate', () => {
const actual = jest.requireActual('hooks/useSafeNavigate');
return {
...actual,
useSafeNavigate: (): any => ({
safeNavigate: mockSafeNavigate,
}),
};
});
const mockSetQuery = jest.fn();
const mockUrlQuery = {

View File

@@ -158,8 +158,7 @@
}
}
.log-scale,
.decimal-precision-selector {
.log-scale {
margin-top: 16px;
display: flex;
justify-content: space-between;

View File

@@ -192,17 +192,3 @@ export const panelTypeVsContextLinks: {
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsDecimalPrecision: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: true,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@@ -12,10 +12,6 @@ import {
Switch,
Typography,
} from 'antd';
import {
PrecisionOption,
PrecisionOptionsEnum,
} from 'components/Graph/yAxisConfig';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import GraphTypes, {
@@ -52,7 +48,6 @@ import {
panelTypeVsColumnUnitPreferences,
panelTypeVsContextLinks,
panelTypeVsCreateAlert,
panelTypeVsDecimalPrecision,
panelTypeVsFillSpan,
panelTypeVsLegendColors,
panelTypeVsLegendPosition,
@@ -100,8 +95,6 @@ function RightContainer({
selectedTime,
yAxisUnit,
setYAxisUnit,
decimalPrecision,
setDecimalPrecision,
setGraphHandler,
thresholds,
combineHistogram,
@@ -167,7 +160,6 @@ function RightContainer({
panelTypeVsColumnUnitPreferences[selectedGraph];
const allowContextLinks =
panelTypeVsContextLinks[selectedGraph] && enableDrillDown;
const allowDecimalPrecision = panelTypeVsDecimalPrecision[selectedGraph];
const { currentQuery } = useQueryBuilder();
@@ -364,30 +356,6 @@ function RightContainer({
}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
@@ -585,8 +553,6 @@ interface RightContainerProps {
setBucketWidth: Dispatch<SetStateAction<number>>;
setBucketCount: Dispatch<SetStateAction<number>>;
setYAxisUnit: Dispatch<SetStateAction<string>>;
decimalPrecision: PrecisionOption;
setDecimalPrecision: Dispatch<SetStateAction<PrecisionOption>>;
setGraphHandler: (type: PANEL_TYPES) => void;
thresholds: ThresholdProps[];
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;

View File

@@ -4,10 +4,6 @@ import './NewWidget.styles.scss';
import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import {
PrecisionOption,
PrecisionOptionsEnum,
} from 'components/Graph/yAxisConfig';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { adjustQueryForV5 } from 'components/QueryBuilderV2/utils';
import { QueryParams } from 'constants/query';
@@ -182,10 +178,6 @@ function NewWidget({
selectedWidget?.yAxisUnit || 'none',
);
const [decimalPrecision, setDecimalPrecision] = useState<PrecisionOption>(
selectedWidget?.decimalPrecision ?? PrecisionOptionsEnum.TWO,
);
const [stackedBarChart, setStackedBarChart] = useState<boolean>(
selectedWidget?.stackedBarChart || false,
);
@@ -265,7 +257,6 @@ function NewWidget({
opacity,
nullZeroValues: selectedNullZeroValue,
yAxisUnit,
decimalPrecision,
thresholds,
softMin,
softMax,
@@ -299,7 +290,6 @@ function NewWidget({
thresholds,
title,
yAxisUnit,
decimalPrecision,
bucketWidth,
bucketCount,
combineHistogram,
@@ -503,8 +493,6 @@ function NewWidget({
title: selectedWidget?.title,
stackedBarChart: selectedWidget?.stackedBarChart || false,
yAxisUnit: selectedWidget?.yAxisUnit,
decimalPrecision:
selectedWidget?.decimalPrecision || PrecisionOptionsEnum.TWO,
panelTypes: graphType,
query: adjustedQueryForV5,
thresholds: selectedWidget?.thresholds,
@@ -534,8 +522,6 @@ function NewWidget({
title: selectedWidget?.title,
stackedBarChart: selectedWidget?.stackedBarChart || false,
yAxisUnit: selectedWidget?.yAxisUnit,
decimalPrecision:
selectedWidget?.decimalPrecision || PrecisionOptionsEnum.TWO,
panelTypes: graphType,
query: adjustedQueryForV5,
thresholds: selectedWidget?.thresholds,
@@ -850,8 +836,6 @@ function NewWidget({
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}

View File

@@ -1,6 +1,5 @@
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { PrecisionOptionsEnum } from 'components/Graph/yAxisConfig';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
@@ -555,7 +554,6 @@ export const getDefaultWidgetData = (
softMax: null,
softMin: null,
stackedBarChart: name === PANEL_TYPES.BAR,
decimalPrecision: PrecisionOptionsEnum.TWO, // default decimal precision
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
...field,
type: field.fieldContext ?? '',

View File

@@ -347,7 +347,6 @@ function OnboardingAddDataSource(): JSX.Element {
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SEARCHED}`,
{
searchedDataSource: query,
resultCount: filteredDataSources.length,
},
);
}, 300);

View File

@@ -26,7 +26,6 @@ function HistogramPanelWrapper({
enableDrillDown = false,
}: PanelWrapperProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const legendScrollPositionRef = useRef<number>(0);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
@@ -130,10 +129,6 @@ function HistogramPanelWrapper({
onClickHandler: enableDrillDown
? clickHandlerWithContextMenu
: onClickHandler ?? _noop,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: number) => {
legendScrollPositionRef.current = position;
},
}),
[
containerDimensions,

View File

@@ -104,7 +104,6 @@ function PiePanelWrapper({
const formattedTotal = getYAxisFormattedValue(
totalValue.toString(),
widget?.yAxisUnit || 'none',
widget?.decimalPrecision,
);
// Extract numeric part and unit separately for styling
@@ -220,7 +219,6 @@ function PiePanelWrapper({
const displayValue = getYAxisFormattedValue(
arc.data.value,
widget?.yAxisUnit || 'none',
widget?.decimalPrecision,
);
// Determine text anchor based on position in the circle

View File

@@ -40,7 +40,6 @@ function TablePanelWrapper({
enableDrillDown={enableDrillDown}
panelType={widget.panelTypes}
queryRangeRequest={queryRangeRequest}
decimalPrecision={widget.decimalPrecision}
// eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG}
/>

View File

@@ -249,7 +249,6 @@ function UplotPanelWrapper({
}) => {
legendScrollPositionRef.current = position;
},
decimalPrecision: widget.decimalPrecision,
}),
[
queryResponse.data?.payload,

View File

@@ -27,7 +27,7 @@ describe('Value panel wrappper tests', () => {
);
// selected y axis unit as miliseconds (ms)
expect(getByText('295.43')).toBeInTheDocument();
expect(getByText('295')).toBeInTheDocument();
expect(getByText('ms')).toBeInTheDocument();
});

View File

@@ -330,7 +330,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
431.25 ms
431 ms
</div>
</div>
</div>
@@ -368,7 +368,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
431.25 ms
431 ms
</div>
</div>
</div>
@@ -406,7 +406,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
287.11 ms
287 ms
</div>
</div>
</div>
@@ -444,7 +444,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
230.02 ms
230 ms
</div>
</div>
</div>
@@ -482,7 +482,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<div
class="line-clamped-wrapper__text"
>
66.37 ms
66.4 ms
</div>
</div>
</div>

View File

@@ -51,7 +51,7 @@ exports[`Value panel wrappper tests should render tooltip when there are conflic
class="ant-typography value-graph-text css-dev-only-do-not-override-2i2tap"
style="color: Blue; font-size: 16px;"
>
295.43
295
</span>
<span
class="ant-typography value-graph-unit css-dev-only-do-not-override-2i2tap"

View File

@@ -1,118 +0,0 @@
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
import uPlot from 'uplot';
// Mock dependencies
jest.mock('lib/uPlotLib/plugins/tooltipPlugin', () => jest.fn(() => ({})));
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => jest.fn(() => ({})));
const mockApiResponse = {
data: {
result: [
{
metric: { __name__: 'test_metric' },
queryName: 'test_query',
values: [[1640995200, '10'] as [number, string]],
},
],
resultType: 'time_series',
newResult: {
data: {
result: [],
resultType: 'time_series',
},
},
},
};
const mockDimensions = { width: 800, height: 400 };
const mockHistogramData: uPlot.AlignedData = [[1640995200], [10]];
const TEST_HISTOGRAM_ID = 'test-histogram';
describe('Histogram Chart Options Legend Scroll Position', () => {
let originalRequestAnimationFrame: typeof global.requestAnimationFrame;
beforeEach(() => {
jest.clearAllMocks();
originalRequestAnimationFrame = global.requestAnimationFrame;
});
afterEach(() => {
global.requestAnimationFrame = originalRequestAnimationFrame;
});
it('should set up scroll position tracking in histogram chart ready hook', () => {
const mockSetScrollPosition = jest.fn();
const options = getUplotHistogramChartOptions({
id: TEST_HISTOGRAM_ID,
dimensions: mockDimensions,
isDarkMode: false,
apiResponse: mockApiResponse,
histogramData: mockHistogramData,
legendScrollPosition: 0,
setLegendScrollPosition: mockSetScrollPosition,
});
// Create mock chart with legend element
const mockChart = ({
root: document.createElement('div'),
} as unknown) as uPlot;
const legend = document.createElement('div');
legend.className = 'u-legend';
mockChart.root.appendChild(legend);
const addEventListenerSpy = jest.spyOn(legend, 'addEventListener');
// Execute ready hook
if (options.hooks?.ready) {
options.hooks.ready.forEach((hook) => hook?.(mockChart));
}
// Verify that scroll event listener was added and cleanup function was stored
expect(addEventListenerSpy).toHaveBeenCalledWith(
'scroll',
expect.any(Function),
);
expect(
(mockChart as uPlot & { _legendScrollCleanup?: () => void })
._legendScrollCleanup,
).toBeDefined();
});
it('should restore histogram chart scroll position when provided', () => {
const mockScrollPosition = 50;
const mockSetScrollPosition = jest.fn();
const options = getUplotHistogramChartOptions({
id: TEST_HISTOGRAM_ID,
dimensions: mockDimensions,
isDarkMode: false,
apiResponse: mockApiResponse,
histogramData: mockHistogramData,
legendScrollPosition: mockScrollPosition,
setLegendScrollPosition: mockSetScrollPosition,
});
// Create mock chart with legend element
const mockChart = ({
root: document.createElement('div'),
} as unknown) as uPlot;
const legend = document.createElement('div');
legend.className = 'u-legend';
legend.scrollTop = 0;
mockChart.root.appendChild(legend);
// Mock requestAnimationFrame
const mockRequestAnimationFrame = jest.fn((callback) => callback());
global.requestAnimationFrame = mockRequestAnimationFrame;
// Execute ready hook
if (options.hooks?.ready) {
options.hooks.ready.forEach((hook) => hook?.(mockChart));
}
// Verify that requestAnimationFrame was called and scroll position was restored
expect(mockRequestAnimationFrame).toHaveBeenCalledWith(expect.any(Function));
expect(legend.scrollTop).toBe(mockScrollPosition);
});
});

View File

@@ -81,13 +81,6 @@ function TimeSeriesView({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const legendScrollPositionRef = useRef<{
scrollTop: number;
scrollLeft: number;
}>({
scrollTop: 0,
scrollLeft: 0,
});
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
@@ -210,13 +203,6 @@ function TimeSeriesView({
setGraphsVisibilityStates: setGraphVisibility,
enhancedLegend: true,
legendPosition: LegendPosition.BOTTOM,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
scrollLeft: number;
}) => {
legendScrollPositionRef.current = position;
},
});
return (

View File

@@ -1,7 +1,14 @@
import { cloneDeep, isEqual } from 'lodash-es';
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import {
Location,
NavigateFunction,
useLocation,
useNavigate,
} from 'react-router-dom-v5-compat';
import { isEventObject } from 'utils/isEventObject';
// state uses 'any' because react-router's NavigateOptions interface uses it
interface NavigateOptions {
replace?: boolean;
state?: any;
@@ -83,6 +90,74 @@ const isDefaultNavigation = (currentUrl: URL, targetUrl: URL): boolean => {
return newKeys.length > 0;
};
// Helper function to extract options from arguments
const extractOptions = (
optionsOrEvent?:
| NavigateOptions
| React.MouseEvent
| MouseEvent
| KeyboardEvent,
options?: NavigateOptions,
): NavigateOptions => {
const isEvent = isEventObject(optionsOrEvent);
const actualOptions = isEvent ? options : (optionsOrEvent as NavigateOptions);
const shouldOpenInNewTab =
isEvent && (optionsOrEvent.metaKey || optionsOrEvent.ctrlKey);
return {
...actualOptions,
newTab: shouldOpenInNewTab || actualOptions?.newTab,
};
};
// Helper function to create target URL
const createTargetUrl = (
to: string | SafeNavigateParams,
location: Location,
): URL => {
if (typeof to === 'string') {
return new URL(to, window.location.origin);
}
return new URL(
`${to.pathname || location.pathname}${to.search || ''}`,
window.location.origin,
);
};
// Helper function to handle new tab navigation
const handleNewTabNavigation = (
to: string | SafeNavigateParams,
location: Location,
): void => {
const targetPath =
typeof to === 'string'
? to
: `${to.pathname || location.pathname}${to.search || ''}`;
window.open(targetPath, '_blank');
};
// Helper function to perform navigation
const performNavigation = (
to: string | SafeNavigateParams,
navigationOptions: NavigateOptions,
navigate: NavigateFunction,
location: Location,
): void => {
if (typeof to === 'string') {
navigate(to, navigationOptions);
} else {
navigate(
{
pathname: to.pathname || location.pathname,
search: to.search,
},
navigationOptions,
);
}
};
export const useSafeNavigate = (
{ preventSameUrlNavigation }: UseSafeNavigateProps = {
preventSameUrlNavigation: true,
@@ -90,6 +165,11 @@ export const useSafeNavigate = (
): {
safeNavigate: (
to: string | SafeNavigateParams,
optionsOrEvent?:
| NavigateOptions
| React.MouseEvent
| MouseEvent
| KeyboardEvent,
options?: NavigateOptions,
) => void;
} => {
@@ -97,30 +177,25 @@ export const useSafeNavigate = (
const location = useLocation();
const safeNavigate = useCallback(
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
(
to: string | SafeNavigateParams,
optionsOrEvent?:
| NavigateOptions
| React.MouseEvent
| MouseEvent
| KeyboardEvent,
options?: NavigateOptions,
) => {
const finalOptions = extractOptions(optionsOrEvent, options);
const currentUrl = new URL(
`${location.pathname}${location.search}`,
window.location.origin,
);
const targetUrl = createTargetUrl(to, location);
let targetUrl: URL;
if (typeof to === 'string') {
targetUrl = new URL(to, window.location.origin);
} else {
targetUrl = new URL(
`${to.pathname || location.pathname}${to.search || ''}`,
window.location.origin,
);
}
// If newTab is true, open in new tab and return early
if (options?.newTab) {
const targetPath =
typeof to === 'string'
? to
: `${to.pathname || location.pathname}${to.search || ''}`;
window.open(targetPath, '_blank');
// Handle new tab navigation
if (finalOptions?.newTab) {
handleNewTabNavigation(to, location);
return;
}
@@ -132,23 +207,13 @@ export const useSafeNavigate = (
}
const navigationOptions = {
...options,
replace: isDefaultParamsNavigation || options?.replace,
...finalOptions,
replace: isDefaultParamsNavigation || finalOptions?.replace,
};
if (typeof to === 'string') {
navigate(to, navigationOptions);
} else {
navigate(
{
pathname: to.pathname || location.pathname,
search: to.search,
},
navigationOptions,
);
}
performNavigation(to, navigationOptions, navigate, location);
},
[navigate, location.pathname, location.search, preventSameUrlNavigation],
[navigate, location, preventSameUrlNavigation],
);
return { safeNavigate };

View File

@@ -0,0 +1,634 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { LocationDescriptorObject } from 'history';
import history from '../history';
jest.mock('history', () => {
const actualHistory = jest.requireActual('history');
const mockPush = jest.fn();
const mockReplace = jest.fn();
const mockGo = jest.fn();
const mockGoBack = jest.fn();
const mockGoForward = jest.fn();
const mockBlock = jest.fn(() => jest.fn());
const mockListen = jest.fn(() => jest.fn());
const mockCreateHref = jest.fn((location) => {
if (typeof location === 'string') return location;
return actualHistory.createPath(location);
});
const baseHistory = {
length: 2,
action: 'PUSH' as const,
location: {
pathname: '/current-path',
search: '?existing=param',
hash: '#section',
state: { existing: 'state' },
key: 'test-key',
},
push: mockPush,
replace: mockReplace,
go: mockGo,
goBack: mockGoBack,
goForward: mockGoForward,
block: mockBlock,
listen: mockListen,
createHref: mockCreateHref,
};
return {
...actualHistory,
createBrowserHistory: jest.fn(() => baseHistory),
};
});
interface TestUser {
id: number;
name: string;
email: string;
}
interface TestState {
from?: string;
user?: TestUser;
timestamp?: number;
}
describe('Enhanced History Methods', () => {
let mockWindowOpen: jest.SpyInstance;
let originalPush: jest.MockedFunction<typeof history.push>;
beforeEach(() => {
jest.clearAllMocks();
mockWindowOpen = jest.spyOn(window, 'open').mockImplementation(() => null);
originalPush = history.originalPush as jest.MockedFunction<
typeof history.push
>;
});
afterEach(() => {
mockWindowOpen.mockRestore();
});
describe('history.push() - String Path Navigation', () => {
it('should handle simple string path navigation', () => {
history.push('/dashboard');
expect(originalPush).toHaveBeenCalledTimes(1);
expect(originalPush).toHaveBeenCalledWith('/dashboard', undefined);
expect(mockWindowOpen).not.toHaveBeenCalled();
});
it('should handle string path with state', () => {
const testState: TestState = { from: 'home', timestamp: Date.now() };
history.push('/dashboard', testState);
expect(originalPush).toHaveBeenCalledWith('/dashboard', testState);
expect(mockWindowOpen).not.toHaveBeenCalled();
});
it('should handle string path with query parameters', () => {
history.push('/logs?filter=error&timeRange=24h');
expect(originalPush).toHaveBeenCalledWith(
'/logs?filter=error&timeRange=24h',
undefined,
);
});
it('should handle string path with hash', () => {
history.push('/docs#installation');
expect(originalPush).toHaveBeenCalledWith('/docs#installation', undefined);
});
it('should handle complex URL with all components', () => {
const complexUrl = '/api/traces?service=backend&status=error#span-details';
const state: TestState = {
user: { id: 1, name: 'John', email: 'john@test.com' },
};
history.push(complexUrl, state);
expect(originalPush).toHaveBeenCalledWith(complexUrl, state);
});
});
describe('history.push() - Location Object Navigation', () => {
it('should handle location object with only pathname', () => {
const location: LocationDescriptorObject = {
pathname: '/metrics',
};
history.push(location);
expect(originalPush).toHaveBeenCalledWith(location, undefined);
expect(mockWindowOpen).not.toHaveBeenCalled();
});
it('should handle location object with pathname and search', () => {
const location: LocationDescriptorObject = {
pathname: '/logs',
search: '?filter=error&severity=high',
};
history.push(location);
expect(originalPush).toHaveBeenCalledWith(location, undefined);
});
it('should handle location object with all properties', () => {
const location: LocationDescriptorObject<TestState> = {
pathname: '/traces',
search: '?service=api-server&duration=slow',
hash: '#span-123',
state: { from: 'dashboard', timestamp: Date.now() },
key: 'unique-key',
};
history.push(location);
expect(originalPush).toHaveBeenCalledWith(location, undefined);
});
it('should handle location object with state passed separately', () => {
const location: LocationDescriptorObject = {
pathname: '/alerts',
search: '?type=critical',
};
const separateState: TestState = { from: 'monitoring' };
history.push(location, separateState);
expect(originalPush).toHaveBeenCalledWith(location, separateState);
});
it('should handle empty location object', () => {
const location: LocationDescriptorObject = {};
history.push(location);
expect(originalPush).toHaveBeenCalledWith(location, undefined);
});
it('should preserve current pathname when updating search', () => {
const location: LocationDescriptorObject = {
pathname: history.location.pathname,
search: '?newParam=value',
};
history.push(location);
expect(originalPush).toHaveBeenCalledWith(location, undefined);
expect(originalPush.mock.calls[0][0]).toHaveProperty(
'pathname',
'/current-path',
);
});
});
describe('history.push() - Event Handling (Cmd/Ctrl+Click)', () => {
describe('MouseEvent handling', () => {
it('should open in new tab when metaKey is pressed with string path', () => {
const event = new MouseEvent('click', { metaKey: true });
history.push('/dashboard', event);
expect(mockWindowOpen).toHaveBeenCalledWith('/dashboard', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should open in new tab when ctrlKey is pressed with string path', () => {
const event = new MouseEvent('click', { ctrlKey: true });
history.push('/metrics', event);
expect(mockWindowOpen).toHaveBeenCalledWith('/metrics', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should open in new tab when both metaKey and ctrlKey are pressed', () => {
const event = new MouseEvent('click', { metaKey: true, ctrlKey: true });
history.push('/logs', event);
expect(mockWindowOpen).toHaveBeenCalledWith('/logs', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should handle normal click without meta/ctrl keys', () => {
const event = new MouseEvent('click', { metaKey: false, ctrlKey: false });
const state: TestState = { from: 'nav' };
history.push('/alerts', event, state);
expect(mockWindowOpen).not.toHaveBeenCalled();
expect(originalPush).toHaveBeenCalledWith('/alerts', state);
});
});
describe('KeyboardEvent handling', () => {
it('should open in new tab when metaKey is pressed with keyboard event', () => {
const event = new KeyboardEvent('keydown', { metaKey: true });
history.push('/traces', event);
expect(mockWindowOpen).toHaveBeenCalledWith('/traces', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should open in new tab when ctrlKey is pressed with keyboard event', () => {
const event = new KeyboardEvent('keydown', { ctrlKey: true });
history.push('/pipelines', event);
expect(mockWindowOpen).toHaveBeenCalledWith('/pipelines', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
});
describe('React SyntheticEvent handling', () => {
it('should handle React MouseEvent with metaKey', () => {
const nativeEvent = new MouseEvent('click', { metaKey: true });
const reactEvent = {
nativeEvent,
metaKey: true,
ctrlKey: false,
} as React.MouseEvent;
history.push('/dashboard', reactEvent);
expect(mockWindowOpen).toHaveBeenCalledWith('/dashboard', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should handle React MouseEvent with ctrlKey', () => {
const nativeEvent = new MouseEvent('click', { ctrlKey: true });
const reactEvent = {
nativeEvent,
metaKey: false,
ctrlKey: true,
} as React.MouseEvent;
history.push('/logs', reactEvent);
expect(mockWindowOpen).toHaveBeenCalledWith('/logs', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should handle React MouseEvent without modifier keys', () => {
const nativeEvent = new MouseEvent('click');
const reactEvent = {
nativeEvent,
metaKey: false,
ctrlKey: false,
} as React.MouseEvent;
history.push('/metrics', reactEvent);
expect(mockWindowOpen).not.toHaveBeenCalled();
expect(originalPush).toHaveBeenCalledWith('/metrics', undefined);
});
});
describe('Location Object with Event handling', () => {
it('should open location object URL in new tab with metaKey', () => {
const location: LocationDescriptorObject = {
pathname: '/traces',
search: '?service=backend',
hash: '#span-details',
};
const event = new MouseEvent('click', { metaKey: true });
history.push(location, event);
expect(mockWindowOpen).toHaveBeenCalledWith(
'/traces?service=backend#span-details',
'_blank',
);
expect(originalPush).not.toHaveBeenCalled();
});
it('should open location object URL in new tab with ctrlKey', () => {
const location: LocationDescriptorObject = {
pathname: '/alerts',
search: '?status=firing',
};
const event = new MouseEvent('click', { ctrlKey: true });
history.push(location, event);
expect(mockWindowOpen).toHaveBeenCalledWith(
'/alerts?status=firing',
'_blank',
);
expect(originalPush).not.toHaveBeenCalled();
});
it('should handle location object with normal navigation', () => {
const location: LocationDescriptorObject = {
pathname: '/dashboard',
search: '?tab=overview',
};
const event = new MouseEvent('click', { metaKey: false, ctrlKey: false });
const state: TestState = { from: 'home' };
history.push(location, event, state);
expect(mockWindowOpen).not.toHaveBeenCalled();
expect(originalPush).toHaveBeenCalledWith(location, state);
});
it('should handle complex location object with all properties in new tab', () => {
const location: LocationDescriptorObject<TestState> = {
pathname: '/api/v1/traces',
search: '?limit=100&offset=0&service=auth',
hash: '#result-section',
state: { from: 'explorer' }, // State is ignored in new tab
};
const event = new MouseEvent('click', { metaKey: true });
history.push(location, event);
expect(mockWindowOpen).toHaveBeenCalledWith(
'/api/v1/traces?limit=100&offset=0&service=auth#result-section',
'_blank',
);
});
});
});
describe('history.push() - Edge Cases and Error Scenarios', () => {
it('should handle undefined as second parameter', () => {
history.push('/dashboard', undefined);
expect(originalPush).toHaveBeenCalledWith('/dashboard', undefined);
});
it('should handle null as second parameter', () => {
history.push('/logs', null);
expect(originalPush).toHaveBeenCalledWith('/logs', null);
});
it('should handle empty string path', () => {
history.push('');
expect(originalPush).toHaveBeenCalledWith('', undefined);
});
it('should handle root path', () => {
history.push('/');
expect(originalPush).toHaveBeenCalledWith('/', undefined);
});
it('should handle relative paths', () => {
history.push('../parent');
expect(originalPush).toHaveBeenCalledWith('../parent', undefined);
});
it('should handle special characters in path', () => {
const specialPath = '/path/with spaces/and#special?chars=@$%';
history.push(specialPath);
expect(originalPush).toHaveBeenCalledWith(specialPath, undefined);
});
it('should handle location object with undefined values', () => {
const location: LocationDescriptorObject = {
pathname: undefined,
search: undefined,
hash: undefined,
state: undefined,
};
history.push(location);
expect(originalPush).toHaveBeenCalledWith(location, undefined);
});
it('should handle very long URLs', () => {
const longParam = 'x'.repeat(1000);
const longUrl = `/path?param=${longParam}`;
history.push(longUrl);
expect(originalPush).toHaveBeenCalledWith(longUrl, undefined);
});
it('should handle object that looks like an event but isnt', () => {
const fakeEvent = {
metaKey: 'not-a-boolean', // Invalid type but still truthy values
ctrlKey: 'not-a-boolean',
};
history.push('/dashboard', fakeEvent as any);
// The implementation checks if metaKey/ctrlKey exist and are truthy values
// Since these are truthy strings, it will be treated as an event
expect(mockWindowOpen).toHaveBeenCalledWith('/dashboard', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should handle event-like object with falsy values', () => {
const fakeEventFalsy = {
metaKey: false,
ctrlKey: false,
};
history.push('/dashboard', fakeEventFalsy as any);
// The object is detected as an event (has metaKey/ctrlKey properties)
// but since both are false, it doesn't open in new tab
// When treated as event, third param (state) is undefined
expect(mockWindowOpen).not.toHaveBeenCalled();
expect(originalPush).toHaveBeenCalledWith('/dashboard', undefined);
});
it('should handle partial event-like objects', () => {
const partialEvent = { metaKey: true }; // Has metaKey but not instanceof MouseEvent
history.push('/logs', partialEvent as any);
expect(mockWindowOpen).toHaveBeenCalledWith('/logs', '_blank');
expect(originalPush).not.toHaveBeenCalled();
});
it('should handle object without event properties as state', () => {
const regularObject = {
someData: 'value',
anotherProp: 123,
// No metaKey or ctrlKey properties
};
history.push('/page', regularObject);
// Object without metaKey/ctrlKey is treated as state, not event
expect(mockWindowOpen).not.toHaveBeenCalled();
expect(originalPush).toHaveBeenCalledWith('/page', regularObject);
});
});
describe('history.push() - State Handling', () => {
it('should pass state with string path', () => {
const complexState: TestState = {
from: 'dashboard',
user: { id: 123, name: 'Test User', email: 'test@example.com' },
timestamp: Date.now(),
};
history.push('/profile', complexState);
expect(originalPush).toHaveBeenCalledWith('/profile', complexState);
});
it('should handle state with location object', () => {
const location: LocationDescriptorObject<TestState> = {
pathname: '/settings',
state: { from: 'profile' },
};
const additionalState: TestState = { timestamp: Date.now() };
history.push(location, additionalState);
expect(originalPush).toHaveBeenCalledWith(location, additionalState);
});
it('should handle state with event and string path', () => {
const event = new MouseEvent('click', { metaKey: false });
const state: TestState = { from: 'nav' };
history.push('/dashboard', event, state);
expect(originalPush).toHaveBeenCalledWith('/dashboard', state);
});
it('should handle state with event and location object', () => {
const location: LocationDescriptorObject = {
pathname: '/logs',
};
const event = new MouseEvent('click', { metaKey: false });
const state: TestState = { from: 'sidebar' };
history.push(location, event, state);
expect(originalPush).toHaveBeenCalledWith(location, state);
});
});
describe('Other History Methods', () => {
it('should have working replace method', () => {
// replace should exist and be callable
expect(history.replace).toBeDefined();
expect(typeof history.replace).toBe('function');
history.replace('/new-path');
const mockReplace = (history as any).replace as jest.MockedFunction<
typeof history.replace
>;
expect(mockReplace).toHaveBeenCalledWith('/new-path');
});
it('should have working go method', () => {
expect(history.go).toBeDefined();
expect(typeof history.go).toBe('function');
history.go(-2);
const mockGo = (history as any).go as jest.MockedFunction<typeof history.go>;
expect(mockGo).toHaveBeenCalledWith(-2);
});
it('should have working goBack method', () => {
expect(history.goBack).toBeDefined();
expect(typeof history.goBack).toBe('function');
history.goBack();
const mockGoBack = (history as any).goBack as jest.MockedFunction<
typeof history.goBack
>;
expect(mockGoBack).toHaveBeenCalled();
});
it('should have working goForward method', () => {
expect(history.goForward).toBeDefined();
expect(typeof history.goForward).toBe('function');
history.goForward();
const mockGoForward = (history as any).goForward as jest.MockedFunction<
typeof history.goForward
>;
expect(mockGoForward).toHaveBeenCalled();
});
it('should have working block method', () => {
expect(history.block).toBeDefined();
expect(typeof history.block).toBe('function');
const unblock = history.block('Are you sure?');
expect(typeof unblock).toBe('function');
const mockBlock = (history as any).block as jest.MockedFunction<
typeof history.block
>;
expect(mockBlock).toHaveBeenCalledWith('Are you sure?');
});
it('should have working listen method', () => {
expect(history.listen).toBeDefined();
expect(typeof history.listen).toBe('function');
const listener = jest.fn();
const unlisten = history.listen(listener);
expect(typeof unlisten).toBe('function');
const mockListen = (history as any).listen as jest.MockedFunction<
typeof history.listen
>;
expect(mockListen).toHaveBeenCalledWith(listener);
});
it('should have working createHref method', () => {
expect(history.createHref).toBeDefined();
expect(typeof history.createHref).toBe('function');
const location: LocationDescriptorObject = {
pathname: '/test',
search: '?query=value',
};
const href = history.createHref(location);
expect(href).toBe('/test?query=value');
});
it('should have accessible location property', () => {
expect(history.location).toBeDefined();
expect(history.location.pathname).toBe('/current-path');
expect(history.location.search).toBe('?existing=param');
expect(history.location.hash).toBe('#section');
expect(history.location.state).toEqual({ existing: 'state' });
});
it('should have accessible length property', () => {
expect(history.length).toBeDefined();
expect(history.length).toBe(2);
});
it('should have accessible action property', () => {
expect(history.action).toBeDefined();
expect(history.action).toBe('PUSH');
});
});
});

View File

@@ -1,3 +1,58 @@
import { createBrowserHistory } from 'history';
import {
createBrowserHistory,
createPath,
History,
LocationDescriptorObject,
LocationState,
} from 'history';
import { isEventObject } from 'utils/isEventObject';
export default createBrowserHistory();
// Create the base history instance
const baseHistory = createBrowserHistory();
type PathOrLocation = string | LocationDescriptorObject<LocationState>;
// Extend the History interface to include enhanced push method
interface EnhancedHistory extends History {
push: {
(path: PathOrLocation, state?: any): void;
(
path: PathOrLocation,
event?: React.MouseEvent | MouseEvent | KeyboardEvent,
state?: any,
): void;
};
originalPush: History['push'];
}
// Create enhanced history with overridden push method
const history = baseHistory as EnhancedHistory;
// Store the original push method
history.originalPush = baseHistory.push;
// Override push to handle meta/ctrl key events and location objects
history.push = function (
path: PathOrLocation,
eventOrState?: React.MouseEvent | MouseEvent | KeyboardEvent | any,
state?: any,
): void {
// Check if second argument is an event object
const isEvent = isEventObject(eventOrState);
// If it's an event and meta/ctrl key is pressed, open in new tab
if (isEvent && (eventOrState.metaKey || eventOrState.ctrlKey)) {
// Convert location object to URL string using createPath from history
const url = typeof path === 'string' ? path : createPath(path);
window.open(url, '_blank');
return;
}
// Otherwise, use normal navigation
// The original push method already handles both strings and location objects
// If eventOrState is not an event, treat it as state
const actualState = isEvent ? state : eventOrState;
history.originalPush(path, actualState);
};
export default history;

View File

@@ -47,7 +47,6 @@ export interface GetUPlotChartOptions {
panelType?: PANEL_TYPES;
onDragSelect?: (startTime: number, endTime: number) => void;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
onClickHandler?: OnClickPluginOpts['onClick'];
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates?: FullViewProps['setGraphsVisibilityStates'];
@@ -193,7 +192,6 @@ export const getUPlotChartOptions = ({
apiResponse,
onDragSelect,
yAxisUnit,
decimalPrecision,
minTimeScale,
maxTimeScale,
onClickHandler = _noop,
@@ -361,7 +359,6 @@ export const getUPlotChartOptions = ({
colorMapping,
customTooltipElement,
query: query || currentQuery,
decimalPrecision,
}),
onClickPlugin({
onClick: onClickHandler,

View File

@@ -17,11 +17,6 @@ import { drawStyles } from './utils/constants';
import { generateColor } from './utils/generateColor';
import getAxes from './utils/getAxes';
// Extended uPlot interface with custom properties
interface ExtendedUPlot extends uPlot {
_legendScrollCleanup?: () => void;
}
type GetUplotHistogramChartOptionsProps = {
id?: string;
apiResponse?: MetricRangePayloadProps;
@@ -35,8 +30,6 @@ type GetUplotHistogramChartOptionsProps = {
setGraphsVisibilityStates?: Dispatch<SetStateAction<boolean[]>>;
mergeAllQueries?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
legendScrollPosition?: number;
setLegendScrollPosition?: (position: number) => void;
};
type GetHistogramSeriesProps = {
@@ -131,8 +124,6 @@ export const getUplotHistogramChartOptions = ({
mergeAllQueries,
onClickHandler = _noop,
panelType,
legendScrollPosition,
setLegendScrollPosition,
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
({
id,
@@ -188,94 +179,33 @@ export const getUplotHistogramChartOptions = ({
(self): void => {
const legend = self.root.querySelector('.u-legend');
if (legend) {
const legendElement = legend as HTMLElement;
// Enhanced legend scroll position preservation
if (setLegendScrollPosition && typeof legendScrollPosition === 'number') {
const handleScroll = (): void => {
setLegendScrollPosition(legendElement.scrollTop);
};
// Add scroll event listener to save position
legendElement.addEventListener('scroll', handleScroll);
// Restore scroll position
requestAnimationFrame(() => {
legendElement.scrollTop = legendScrollPosition;
});
// Store cleanup function
const extSelf = self as ExtendedUPlot;
extSelf._legendScrollCleanup = (): void => {
legendElement.removeEventListener('scroll', handleScroll);
};
}
const seriesEls = legend.querySelectorAll('.u-series');
const seriesArray = Array.from(seriesEls);
seriesArray.forEach((seriesEl, index) => {
// Add click handlers for marker and text separately
const thElement = seriesEl.querySelector('th');
if (thElement) {
const currentMarker = thElement.querySelector('.u-marker');
const textElement =
thElement.querySelector('.legend-text') || thElement;
// Marker click handler - checkbox behavior (toggle individual series)
if (currentMarker) {
currentMarker.addEventListener('click', (e) => {
e.stopPropagation?.(); // Prevent event bubbling to text handler
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
// Toggle the specific series visibility (checkbox behavior)
newGraphVisibilityStates[index + 1] = !newGraphVisibilityStates[
index + 1
];
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
seriesEl.addEventListener('click', () => {
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
if (
newGraphVisibilityStates[index + 1] &&
newGraphVisibilityStates.every((value, i) =>
i === index + 1 ? value : !value,
)
) {
newGraphVisibilityStates.fill(true);
} else {
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[index + 1] = true;
}
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
}
// Text click handler - show only/show all behavior (existing behavior)
textElement.addEventListener('click', (e) => {
e.stopPropagation?.(); // Prevent event bubbling
if (graphsVisibilityStates) {
setGraphsVisibilityStates?.((prev) => {
const newGraphVisibilityStates = [...prev];
// Show only this series / show all behavior
if (
newGraphVisibilityStates[index + 1] &&
newGraphVisibilityStates.every((value, i) =>
i === index + 1 ? value : !value,
)
) {
// If only this series is visible, show all
newGraphVisibilityStates.fill(true);
} else {
// Otherwise, show only this series
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[index + 1] = true;
}
saveLegendEntriesToLocalStorage({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
});
return newGraphVisibilityStates;
});
}
});
}
});
});
}
},

View File

@@ -1,4 +1,4 @@
import { getToolTipValue, PrecisionOption } from 'components/Graph/yAxisConfig';
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { themeColors } from 'constants/theme';
import dayjs from 'dayjs';
@@ -44,7 +44,6 @@ const generateTooltipContent = (
idx: number,
isDarkMode: boolean,
yAxisUnit?: string,
decimalPrecision?: PrecisionOption,
series?: uPlot.Options['series'],
isBillingUsageGraphs?: boolean,
isHistogramGraphs?: boolean,
@@ -128,7 +127,7 @@ const generateTooltipContent = (
let tooltipItemLabel = label;
if (Number.isFinite(value)) {
const tooltipValue = getToolTipValue(value, yAxisUnit, decimalPrecision);
const tooltipValue = getToolTipValue(value, yAxisUnit);
const dataIngestedFormated = getToolTipValue(dataIngested);
if (
duplicatedLegendLabels[label] ||
@@ -240,7 +239,6 @@ type ToolTipPluginProps = {
isBillingUsageGraphs?: boolean;
isHistogramGraphs?: boolean;
isMergedSeries?: boolean;
decimalPrecision?: PrecisionOption;
stackBarChart?: boolean;
isDarkMode: boolean;
customTooltipElement?: HTMLDivElement;
@@ -261,7 +259,6 @@ const tooltipPlugin = ({
timezone,
colorMapping,
query,
decimalPrecision,
}: // eslint-disable-next-line sonarjs/cognitive-complexity
ToolTipPluginProps): any => {
let over: HTMLElement;
@@ -323,7 +320,6 @@ ToolTipPluginProps): any => {
idx,
isDarkMode,
yAxisUnit,
decimalPrecision,
u.series,
isBillingUsageGraphs,
isHistogramGraphs,

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { getToolTipValue, PrecisionOption } from 'components/Graph/yAxisConfig';
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { uPlotXAxisValuesFormat } from './constants';
@@ -18,13 +18,11 @@ const getAxes = ({
yAxisUnit,
panelType,
isLogScale,
decimalPrecision,
}: {
isDarkMode: boolean;
yAxisUnit?: string;
panelType?: PANEL_TYPES;
isLogScale?: boolean;
decimalPrecision?: PrecisionOption;
// eslint-disable-next-line sonarjs/cognitive-complexity
}): any => [
{
@@ -63,7 +61,7 @@ const getAxes = ({
if (v === null || v === undefined || Number.isNaN(v)) {
return '';
}
const value = getToolTipValue(v.toString(), yAxisUnit, decimalPrecision);
const value = getToolTipValue(v.toString(), yAxisUnit);
return `${value}`;
}),
gap: 5,

View File

@@ -1,4 +1,3 @@
import { PrecisionOption } from 'components/Graph/yAxisConfig';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
@@ -114,7 +113,6 @@ export interface IBaseWidget {
timePreferance: timePreferenceType;
stepSize?: number;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption; // number of decimals or 'full precision'
stackedBarChart?: boolean;
bucketCount?: number;
bucketWidth?: number;

View File

@@ -0,0 +1,21 @@
// Event types that have metaKey/ctrlKey properties
type EventWithModifiers =
| MouseEvent
| KeyboardEvent
| React.MouseEvent<any, MouseEvent>
| React.KeyboardEvent<any>;
// Helper function to determine if an argument is an event - Also used in utils/history.ts
export const isEventObject = (arg: unknown): arg is EventWithModifiers => {
if (!arg || typeof arg !== 'object') return false;
return (
arg instanceof MouseEvent ||
arg instanceof KeyboardEvent ||
('nativeEvent' in arg &&
(arg.nativeEvent instanceof MouseEvent ||
arg.nativeEvent instanceof KeyboardEvent)) ||
'metaKey' in arg ||
'ctrlKey' in arg
);
};

View File

@@ -6369,13 +6369,13 @@ axe-core@^4.6.2:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz"
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
axios@1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.0.tgz#11248459be05a5ee493485628fa0e4323d0abfc3"
integrity sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==
axios@1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.1.1:
@@ -9677,7 +9677,7 @@ force-graph@1:
kapsule "^1.14"
lodash-es "4"
form-data@4.0.4, form-data@^3.0.0, form-data@^4.0.4:
form-data@4.0.4, form-data@^3.0.0, form-data@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==

View File

@@ -2,6 +2,7 @@ package alertmanagerbatcher
import (
"context"
"io"
"log/slog"
"testing"
@@ -10,7 +11,7 @@ import (
)
func TestBatcherWithOneAlertAndDefaultConfigs(t *testing.T) {
batcher := New(slog.New(slog.DiscardHandler), NewConfig())
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), NewConfig())
_ = batcher.Start(context.Background())
batcher.Add(context.Background(), &alertmanagertypes.PostableAlert{Alert: alertmanagertypes.AlertModel{
@@ -24,7 +25,7 @@ func TestBatcherWithOneAlertAndDefaultConfigs(t *testing.T) {
}
func TestBatcherWithBatchSize(t *testing.T) {
batcher := New(slog.New(slog.DiscardHandler), Config{Size: 2, Capacity: 4})
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
_ = batcher.Start(context.Background())
var alerts alertmanagertypes.PostableAlerts
@@ -44,7 +45,7 @@ func TestBatcherWithBatchSize(t *testing.T) {
}
func TestBatcherWithCClosed(t *testing.T) {
batcher := New(slog.New(slog.DiscardHandler), Config{Size: 2, Capacity: 4})
batcher := New(slog.New(slog.NewTextHandler(io.Discard, nil)), Config{Size: 2, Capacity: 4})
_ = batcher.Start(context.Background())
var alerts alertmanagertypes.PostableAlerts

View File

@@ -2,14 +2,14 @@ package alertmanagerserver
import (
"context"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/prometheus/alertmanager/dispatch"
"io"
"log/slog"
"net/http"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes/alertmanagertypestest"
"github.com/prometheus/alertmanager/dispatch"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/nfroutingstoretest"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
@@ -89,7 +89,7 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
srvCfg := NewConfig()
stateStore := alertmanagertypestest.NewStateStore()
registry := prometheus.NewRegistry()
logger := slog.New(slog.DiscardHandler)
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
server, err := New(context.Background(), logger, registry, srvCfg, orgID, stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, orgID)

View File

@@ -3,6 +3,7 @@ package alertmanagerserver
import (
"bytes"
"context"
"io"
"log/slog"
"net"
"net/http"
@@ -25,7 +26,7 @@ import (
func TestServerSetConfigAndStop(t *testing.T) {
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{GroupInterval: 1 * time.Minute, RepeatInterval: 1 * time.Minute, GroupWait: 1 * time.Minute}, "1")
@@ -37,7 +38,7 @@ func TestServerSetConfigAndStop(t *testing.T) {
func TestServerTestReceiverTypeWebhook(t *testing.T) {
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), NewConfig(), "1", alertmanagertypestest.NewStateStore(), notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(alertmanagertypes.GlobalConfig{}, alertmanagertypes.RouteConfig{GroupInterval: 1 * time.Minute, RepeatInterval: 1 * time.Minute, GroupWait: 1 * time.Minute}, "1")
@@ -85,7 +86,7 @@ func TestServerPutAlerts(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
@@ -133,7 +134,7 @@ func TestServerTestAlert(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")
@@ -238,7 +239,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
srvCfg := NewConfig()
srvCfg.Route.GroupInterval = 1 * time.Second
notificationManager := nfmanagertest.NewMock()
server, err := New(context.Background(), slog.New(slog.DiscardHandler), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
server, err := New(context.Background(), slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry(), srvCfg, "1", stateStore, notificationManager)
require.NoError(t, err)
amConfig, err := alertmanagertypes.NewDefaultConfig(srvCfg.Global, srvCfg.Route, "1")

View File

@@ -2,6 +2,7 @@ package factory
import (
"context"
"io"
"log/slog"
"sync"
"testing"
@@ -32,7 +33,7 @@ func TestRegistryWith2Services(t *testing.T) {
s1 := newTestService(t)
s2 := newTestService(t)
registry, err := NewRegistry(slog.New(slog.DiscardHandler), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
registry, err := NewRegistry(slog.New(slog.NewTextHandler(io.Discard, nil)), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -53,7 +54,7 @@ func TestRegistryWith2ServicesWithoutWait(t *testing.T) {
s1 := newTestService(t)
s2 := newTestService(t)
registry, err := NewRegistry(slog.New(slog.DiscardHandler), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
registry, err := NewRegistry(slog.New(slog.NewTextHandler(io.Discard, nil)), NewNamedService(MustNewName("s1"), s1), NewNamedService(MustNewName("s2"), s2))
require.NoError(t, err)
ctx := context.Background()

View File

@@ -1,6 +1,7 @@
package middleware
import (
"io"
"log/slog"
"net"
"net/http"
@@ -16,7 +17,7 @@ func TestTimeout(t *testing.T) {
writeTimeout := 6 * time.Second
defaultTimeout := 2 * time.Second
maxTimeout := 4 * time.Second
m := NewTimeout(slog.New(slog.DiscardHandler), []string{"/excluded"}, defaultTimeout, maxTimeout)
m := NewTimeout(slog.New(slog.NewTextHandler(io.Discard, nil)), []string{"/excluded"}, defaultTimeout, maxTimeout)
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)

View File

@@ -1,6 +1,7 @@
package instrumentationtest
import (
"io"
"log/slog"
"github.com/SigNoz/signoz/pkg/factory"
@@ -20,7 +21,7 @@ type noopInstrumentation struct {
func New() instrumentation.Instrumentation {
return &noopInstrumentation{
logger: slog.New(slog.DiscardHandler),
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
meterProvider: noopmetric.NewMeterProvider(),
tracerProvider: nooptrace.NewTracerProvider(),
}

View File

@@ -499,9 +499,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/alerts", am.ViewAccess(aH.AlertmanagerAPI.GetAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/keys", am.ViewAccess(aH.getRuleAttributeKeys)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/values", am.ViewAccess(aH.getRuleAttributeValues)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)
@@ -1155,63 +1152,6 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter,
aH.Respond(w, res)
}
func (aH *APIHandler) getRuleAttributeKeys(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get claims from context: %v", err))
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get orgId from claims: %v", err))
return
}
searchText := r.URL.Query().Get("searchText")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || limit <= 0 {
limit = 10
}
keys, err := aH.ruleManager.GetSearchKeys(r.Context(), searchText, limit, orgID)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, keys)
}
func (aH *APIHandler) getRuleAttributeValues(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get claims from context: %v", err))
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get orgId from claims: %v", err))
return
}
attributeKey := r.URL.Query().Get("attributeKey")
if attributeKey == "" {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInvalidInput, "attributeKey is required"))
return
}
searchText := r.URL.Query().Get("searchText")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || limit <= 0 {
limit = 10
}
keys, err := aH.ruleManager.GetSearchValues(r.Context(), searchText, limit, attributeKey, orgID)
if err != nil {
render.Error(w, errorsV2.NewInternalf(errorsV2.CodeInternal, "failed to get rule search values: %v", err))
return
}
render.Success(w, http.StatusOK, keys)
}
func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) {
rules, err := aH.ruleManager.ListRuleStates(r.Context())

View File

@@ -36,16 +36,6 @@ func (s AlertState) String() string {
panic(errors.Errorf("unknown alert state: %d", s))
}
func GetAllRuleStates() []string {
return []string{
StateInactive.String(),
StatePending.String(),
StateFiring.String(),
StateNoData.String(),
StateDisabled.String(),
}
}
func (s AlertState) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"log/slog"
"sort"
"strings"
@@ -1084,60 +1083,3 @@ func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames
return result, nil
}
func (m *Manager) GetSearchKeys(ctx context.Context, searchText string, limit int, orgId valuer.UUID) ([]ruletypes.GetRuleAttributeKeys, error) {
keys, err := m.ruleStore.GetRuleLabelKeys(ctx, searchText, limit, orgId.String())
if err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "failed to get rule label keys: %v", err)
}
result := make([]ruletypes.GetRuleAttributeKeys, len(ruletypes.FixedRuleAttributeKeys))
copy(result, ruletypes.FixedRuleAttributeKeys)
for _, key := range keys {
result = append(result, ruletypes.GetRuleAttributeKeys{
Key: key,
Type: ruletypes.RuleAttributeTypeLabel,
DataType: telemetrytypes.FieldDataTypeString,
})
}
return result, nil
}
func (m *Manager) GetSearchValues(ctx context.Context, searchText string, limit int, key string, orgId valuer.UUID) ([]string, error) {
switch key {
case ruletypes.RuleAttributeKeyChannel:
return m.ruleStore.GetChannel(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyThresholdName:
return m.ruleStore.GetThresholdNames(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyCreatedBy:
return m.ruleStore.GetCreatedBy(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyUpdatedBy:
return m.ruleStore.GetUpdatedBy(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyName:
return m.ruleStore.GetNames(ctx, searchText, limit, orgId.String())
case ruletypes.RuleAttributeKeyState:
allStates := model.GetAllRuleStates()
if searchText == "" {
if limit > 0 && limit < len(allStates) {
return allStates[:limit], nil
}
return allStates, nil
}
filtered := make([]string, 0)
searchLower := strings.ToLower(searchText)
for _, state := range allStates {
if strings.Contains(strings.ToLower(state), searchLower) {
filtered = append(filtered, state)
if limit > 0 && len(filtered) >= limit {
break
}
}
}
return filtered, nil
default:
return m.ruleStore.GetRuleLabelValues(ctx, searchText, limit, key, orgId.String())
}
}

View File

@@ -2,9 +2,6 @@ package sqlrulestore
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/uptrace/bun"
"strings"
"github.com/SigNoz/signoz/pkg/sqlstore"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
@@ -104,205 +101,3 @@ func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.Ru
}
return rule, nil
}
func (r *rule) GetRuleLabelKeys(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
labelKeys := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
elements, elementsAlias := fmter.JSONKeys("data", "$.labels", "keys")
elementsAliasStr := string(fmter.LowerExpression(string(elementsAlias)))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(elementsAliasStr)).
TableExpr("rule, ?", bun.SafeQuery(string(elements))).
Where("? LIKE ?", bun.SafeQuery(elementsAliasStr), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &labelKeys)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "search keys for rule with orgId %s not found", orgId)
}
return labelKeys, nil
}
func (r *rule) GetThresholdNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
// Query threshold spec names
specQuery, specCol := fmter.JSONArrayElements("data", "$.condition.thresholds.spec", "spec")
nameQuery := string(fmter.JSONExtractString(string(specCol), "$.name"))
lowerNameQuery := string(fmter.LowerExpression(nameQuery))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(nameQuery)).
TableExpr("rule, ?", bun.SafeQuery(string(specQuery))).
Where("? LIKE ?", bun.SafeQuery(lowerNameQuery), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "threshold names for rule with orgId %s not found", orgId)
}
if len(names) >= limit {
return names[:limit], nil
}
severityQuery := string(fmter.JSONExtractString("data", "$.labels.severity"))
lowerSeverityQuery := string(fmter.LowerExpression(severityQuery))
thresholds := make([]string, 0)
query = r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(severityQuery)).
TableExpr("rule").
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(lowerSeverityQuery), searchText).
Limit(limit - len(names))
err = query.Scan(ctx, &thresholds)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "threshold names for rule with orgId %s not found", orgId)
}
names = append(names, thresholds...)
return names, nil
}
func (r *rule) GetChannel(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
// Query v2 threshold channels
specSQL, specCol := fmter.JSONArrayElements("data", "$.condition.thresholds.spec", "spec")
channelSQL, channelCol := fmter.JSONArrayOfStrings(string(specCol), "$.channels", "channels")
lowerChannelCol := string(fmter.LowerExpression(string(channelCol)))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(channelCol))).
TableExpr("rule, ?, ?",
bun.SafeQuery(string(specSQL)),
bun.SafeQuery(string(channelSQL))).
Where("? LIKE ?", bun.SafeQuery(lowerChannelCol), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "channel for rule with orgId %s not found", orgId)
}
if len(names) >= limit {
return names[:limit], nil
}
// Query v1 preferred channels
channelsSQL, channelsCol := fmter.JSONArrayOfStrings("data", "$.preferredChannels", "channels")
lowerChannelsCol := fmter.LowerExpression(string(channelsCol))
channels := make([]string, 0)
query = r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(channelsCol))).
TableExpr("rule, ?", bun.SafeQuery(string(channelsSQL))).
Where("? LIKE ?", bun.SafeQuery(string(lowerChannelsCol)), searchText).
Where("org_id = ?", orgId).
Limit(limit - len(names))
err = query.Scan(ctx, &channels)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "channel for rule with orgId %s not found", orgId)
}
names = append(names, channels...)
return names, nil
}
func (r *rule) GetNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
fmter := r.sqlstore.Formatter()
namePath := fmter.JSONExtractString("data", "$.alert")
lowerNamePath := fmter.LowerExpression(string(namePath))
query := r.sqlstore.BunDB().
NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(namePath))).
TableExpr("?", bun.SafeQuery("rule")).
Where("? LIKE ?", bun.SafeQuery(string(lowerNamePath)), searchText).
Where("org_id = ?", orgId).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "names for rule with orgId %s not found", orgId)
}
return names, nil
}
func (r *rule) GetCreatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
query := r.sqlstore.BunDB().NewSelect().
Distinct().
Column("created_by").
TableExpr("?", bun.SafeQuery("rule")).
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(string(r.sqlstore.Formatter().LowerExpression("created_by"))), searchText).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, err
}
return names, nil
}
func (r *rule) GetUpdatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error) {
names := make([]string, 0)
searchText = strings.ToLower(searchText) + "%"
query := r.sqlstore.BunDB().NewSelect().
Distinct().
Column("updated_by").
TableExpr("?", bun.SafeQuery("rule")).
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(string(r.sqlstore.Formatter().LowerExpression("updated_by"))), searchText).
Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, err
}
return names, nil
}
func (r *rule) GetRuleLabelValues(ctx context.Context, searchText string, limit int, labelKey string, orgId string) ([]string, error) {
names := make([]string, 0)
labelPath := r.sqlstore.Formatter().JSONExtractString("data", "$.labels."+labelKey)
searchText = strings.ToLower(searchText) + "%"
query := r.sqlstore.BunDB().NewSelect().
Distinct().
ColumnExpr("?", bun.SafeQuery(string(labelPath))).
TableExpr("?", bun.SafeQuery("rule")).
Where("org_id = ?", orgId).
Where("? LIKE ?", bun.SafeQuery(string(r.sqlstore.Formatter().LowerExpression(string(labelPath)))), searchText).Limit(limit)
err := query.Scan(ctx, &names)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "search values for rule with orgId %s not found", orgId)
}
return names, nil
}

View File

@@ -2,6 +2,7 @@ package signoz
import (
"context"
"io"
"log/slog"
"testing"
@@ -12,7 +13,7 @@ import (
// This is a test to ensure that all fields of config implement the factory.Config interface and are valid with
// their default values.
func TestValidateConfig(t *testing.T) {
logger := slog.New(slog.DiscardHandler)
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
_, err := NewConfig(context.Background(), logger, configtest.NewResolverConfig(), DeprecatedFlags{})
assert.NoError(t, err)
}

View File

@@ -1,113 +0,0 @@
package sqlitesqlstore
import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun/schema"
)
type formatter struct {
bunf schema.Formatter
}
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
return &formatter{bunf: schema.NewFormatter(dialect)}
}
func (f *formatter) JSONExtractString(column, path string) []byte {
var sql []byte
sql = append(sql, "json_extract("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "json_type("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONIsArray(column, path string) []byte {
var sql []byte
sql = append(sql, f.JSONType(column, path)...)
sql = append(sql, " = 'array'"...)
return sql
}
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".value")
}
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
return f.JSONArrayElements(column, path, alias)
}
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".key")
}
func (f *formatter) JSONArrayAgg(expression string) []byte {
var sql []byte
sql = append(sql, "json_group_array("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
if len(values) == 0 {
return []byte("json_array()")
}
var sql []byte
sql = append(sql, "json_array("...)
for i, v := range values {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, '\'')
sql = append(sql, v...)
sql = append(sql, '\'')
}
sql = append(sql, ')')
return sql
}
func (f *formatter) TextToJsonColumn(column string) []byte {
return f.bunf.AppendIdent([]byte{}, column)
}
func (f *formatter) LowerExpression(expression string) []byte {
var sql []byte
sql = append(sql, "lower("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}

View File

@@ -1,397 +0,0 @@
package sqlitesqlstore
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/uptrace/bun/dialect/sqlitedialect"
)
func TestJSONExtractString(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `json_extract("data", '$.field')`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.name",
expected: `json_extract("metadata", '$.user.name')`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `json_extract("json_col", '$')`,
},
{
name: "array index path",
column: "items",
path: "$.list[0]",
expected: `json_extract("items", '$.list[0]')`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONExtractString(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONType(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.field",
expected: `json_type("data", '$.field')`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.age",
expected: `json_type("metadata", '$.user.age')`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `json_type("json_col", '$')`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONType(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONIsArray(t *testing.T) {
tests := []struct {
name string
column string
path string
expected string
}{
{
name: "simple path",
column: "data",
path: "$.items",
expected: `json_type("data", '$.items') = 'array'`,
},
{
name: "nested path",
column: "metadata",
path: "$.user.tags",
expected: `json_type("metadata", '$.user.tags') = 'array'`,
},
{
name: "root path",
column: "json_col",
path: "$",
expected: `json_type("json_col", '$') = 'array'`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONIsArray(tt.column, tt.path))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayElements(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "elem",
expected: `json_each("data") AS "elem"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "elem",
expected: `json_each("data") AS "elem"`,
},
{
name: "nested path",
column: "metadata",
path: "$.items",
alias: "item",
expected: `json_each("metadata", '$.items') AS "item"`,
},
{
name: "deeply nested path",
column: "json_col",
path: "$.user.tags",
alias: "tag",
expected: `json_each("json_col", '$.user.tags') AS "tag"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got, _ := f.JSONArrayElements(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayOfStrings(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "str",
expected: `json_each("data") AS "str"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "str",
expected: `json_each("data") AS "str"`,
},
{
name: "nested path",
column: "metadata",
path: "$.strings",
alias: "s",
expected: `json_each("metadata", '$.strings') AS "s"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got, _ := f.JSONArrayOfStrings(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONKeys(t *testing.T) {
tests := []struct {
name string
column string
path string
alias string
expected string
}{
{
name: "root path with dollar sign",
column: "data",
path: "$",
alias: "k",
expected: `json_each("data") AS "k"`,
},
{
name: "root path empty",
column: "data",
path: "",
alias: "k",
expected: `json_each("data") AS "k"`,
},
{
name: "nested path",
column: "metadata",
path: "$.object",
alias: "key",
expected: `json_each("metadata", '$.object') AS "key"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got, _ := f.JSONKeys(tt.column, tt.path, tt.alias)
assert.Equal(t, tt.expected, string(got))
})
}
}
func TestJSONArrayAgg(t *testing.T) {
tests := []struct {
name string
expression string
expected string
}{
{
name: "simple column",
expression: "id",
expected: "json_group_array(id)",
},
{
name: "expression with function",
expression: "DISTINCT name",
expected: "json_group_array(DISTINCT name)",
},
{
name: "complex expression",
expression: "json_extract(data, '$.field')",
expected: "json_group_array(json_extract(data, '$.field'))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONArrayAgg(tt.expression))
assert.Equal(t, tt.expected, got)
})
}
}
func TestJSONArrayLiteral(t *testing.T) {
tests := []struct {
name string
values []string
expected string
}{
{
name: "empty array",
values: []string{},
expected: "json_array()",
},
{
name: "single value",
values: []string{"value1"},
expected: "json_array('value1')",
},
{
name: "multiple values",
values: []string{"value1", "value2", "value3"},
expected: "json_array('value1', 'value2', 'value3')",
},
{
name: "values with special characters",
values: []string{"test", "with space", "with-dash"},
expected: "json_array('test', 'with space', 'with-dash')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.JSONArrayLiteral(tt.values...))
assert.Equal(t, tt.expected, got)
})
}
}
func TestTextToJsonColumn(t *testing.T) {
tests := []struct {
name string
column string
expected string
}{
{
name: "simple column name",
column: "data",
expected: `"data"`,
},
{
name: "column with underscore",
column: "user_data",
expected: `"user_data"`,
},
{
name: "column with special characters",
column: "json-col",
expected: `"json-col"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.TextToJsonColumn(tt.column))
assert.Equal(t, tt.expected, got)
})
}
}
func TestLowerExpression(t *testing.T) {
tests := []struct {
name string
expr string
expected string
}{
{
name: "json_extract expression",
expr: "json_extract(data, '$.field')",
expected: "lower(json_extract(data, '$.field'))",
},
{
name: "nested json_extract",
expr: "json_extract(metadata, '$.user.name')",
expected: "lower(json_extract(metadata, '$.user.name'))",
},
{
name: "json_type expression",
expr: "json_type(data, '$.field')",
expected: "lower(json_type(data, '$.field'))",
},
{
name: "string concatenation",
expr: "first_name || ' ' || last_name",
expected: "lower(first_name || ' ' || last_name)",
},
{
name: "CAST expression",
expr: "CAST(value AS TEXT)",
expected: "lower(CAST(value AS TEXT))",
},
{
name: "COALESCE expression",
expr: "COALESCE(name, 'default')",
expected: "lower(COALESCE(name, 'default'))",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newFormatter(sqlitedialect.New())
got := string(f.LowerExpression(tt.expr))
assert.Equal(t, tt.expected, got)
})
}
}

View File

@@ -17,11 +17,10 @@ import (
)
type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
formatter sqlstore.SQLFormatter
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
dialect *dialect
}
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
@@ -55,14 +54,11 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
settings.Logger().InfoContext(ctx, "connected to sqlite", "path", config.Sqlite.Path)
sqldb.SetMaxOpenConns(config.Connection.MaxOpenConns)
sqliteDialect := sqlitedialect.New()
bunDB := sqlstore.NewBunDB(settings, sqldb, sqliteDialect, hooks)
return &provider{
settings: settings,
sqldb: sqldb,
bundb: bunDB,
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
settings: settings,
sqldb: sqldb,
bundb: sqlstore.NewBunDB(settings, sqldb, sqlitedialect.New(), hooks),
dialect: new(dialect),
}, nil
}
@@ -78,10 +74,6 @@ func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
func (provider *provider) Formatter() sqlstore.SQLFormatter {
return provider.formatter
}
func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB {
return provider.bundb.BunDBCtx(ctx)
}

View File

@@ -20,8 +20,6 @@ type SQLStore interface {
// Returns the dialect of the database.
Dialect() SQLDialect
Formatter() SQLFormatter
// RunInTxCtx runs the given callback in a transaction. It creates and injects a new context with the transaction.
// If a transaction is present in the context, it will be used.
RunInTxCtx(ctx context.Context, opts *SQLStoreTxOptions, cb func(ctx context.Context) error) error
@@ -88,35 +86,3 @@ type SQLDialect interface {
// as an argument.
ToggleForeignKeyConstraint(ctx context.Context, bun *bun.DB, enable bool) error
}
type SQLFormatter interface {
// JSONExtractString takes path in sqlite format like "$.labels.severity"
JSONExtractString(column, path string) []byte
// JSONType used to determine the type of the value extracted from the path
JSONType(column, path string) []byte
// JSONIsArray used to check whether the value is array or not
JSONIsArray(column, path string) []byte
// JSONArrayElements returns query as well as columns alias to be used for select and where clause
JSONArrayElements(column, path, alias string) ([]byte, []byte)
// JSONArrayOfStrings returns query as well as columns alias to be used for select and where clause
JSONArrayOfStrings(column, path, alias string) ([]byte, []byte)
// JSONArrayAgg aggregates values into a JSON array
JSONArrayAgg(expression string) []byte
// JSONArrayLiteral creates a literal JSON array from the given string values
JSONArrayLiteral(values ...string) []byte
// JSONKeys return extracted key from json as well as alias to be used for select and where clause
JSONKeys(column, path, alias string) ([]byte, []byte)
// TextToJsonColumn converts a text column to JSON type
TextToJsonColumn(column string) []byte
// LowerExpression wraps any SQL expression with lower() function for case-insensitive operations
LowerExpression(expression string) []byte
}

View File

@@ -1,112 +0,0 @@
package sqlstoretest
import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun/schema"
)
type formatter struct {
bunf schema.Formatter
}
func newFormatter(dialect schema.Dialect) sqlstore.SQLFormatter {
return &formatter{bunf: schema.NewFormatter(dialect)}
}
func (f *formatter) JSONExtractString(column, path string) []byte {
var sql []byte
sql = append(sql, "json_extract("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONType(column, path string) []byte {
var sql []byte
sql = append(sql, "json_type("...)
sql = f.bunf.AppendIdent(sql, column)
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "')"...)
return sql
}
func (f *formatter) JSONIsArray(column, path string) []byte {
var sql []byte
sql = append(sql, f.JSONType(column, path)...)
sql = append(sql, " = 'array'"...)
return sql
}
func (f *formatter) JSONArrayElements(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".value")
}
func (f *formatter) JSONArrayOfStrings(column, path, alias string) ([]byte, []byte) {
return f.JSONArrayElements(column, path, alias)
}
func (f *formatter) JSONKeys(column, path, alias string) ([]byte, []byte) {
var sql []byte
sql = append(sql, "json_each("...)
sql = f.bunf.AppendIdent(sql, column)
if path != "$" && path != "" {
sql = append(sql, ", '"...)
sql = append(sql, path...)
sql = append(sql, "'"...)
}
sql = append(sql, ") AS "...)
sql = f.bunf.AppendIdent(sql, alias)
return sql, []byte(alias + ".key")
}
func (f *formatter) JSONArrayAgg(expression string) []byte {
var sql []byte
sql = append(sql, "json_group_array("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}
func (f *formatter) JSONArrayLiteral(values ...string) []byte {
if len(values) == 0 {
return []byte("json_array()")
}
var sql []byte
sql = append(sql, "json_array("...)
for i, v := range values {
if i > 0 {
sql = append(sql, ", "...)
}
sql = append(sql, '\'')
sql = append(sql, v...)
sql = append(sql, '\'')
}
sql = append(sql, ')')
return sql
}
func (f *formatter) TextToJsonColumn(column string) []byte {
return f.bunf.AppendIdent([]byte{}, column)
}
func (f *formatter) LowerExpression(expression string) []byte {
var sql []byte
sql = append(sql, "lower("...)
sql = append(sql, expression...)
sql = append(sql, ')')
return sql
}

View File

@@ -15,11 +15,10 @@ import (
var _ sqlstore.SQLStore = (*Provider)(nil)
type Provider struct {
db *sql.DB
mock sqlmock.Sqlmock
bunDB *bun.DB
dialect *dialect
formatter sqlstore.SQLFormatter
db *sql.DB
mock sqlmock.Sqlmock
bunDB *bun.DB
dialect *dialect
}
func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider {
@@ -39,11 +38,10 @@ func New(config sqlstore.Config, matcher sqlmock.QueryMatcher) *Provider {
}
return &Provider{
db: db,
mock: mock,
bunDB: bunDB,
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
db: db,
mock: mock,
bunDB: bunDB,
dialect: new(dialect),
}
}
@@ -63,8 +61,6 @@ func (provider *Provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}
func (provider *Provider) Formatter() sqlstore.SQLFormatter { return provider.formatter }
func (provider *Provider) BunDBCtx(ctx context.Context) bun.IDB {
return provider.bunDB
}

View File

@@ -4,8 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"slices"
"time"
"unicode/utf8"
@@ -454,18 +452,3 @@ func (g *GettableRule) MarshalJSON() ([]byte, error) {
return json.Marshal(aux)
}
}
type RuleAttributeKeyType struct {
valuer.String
}
var (
RuleAttributeTypeFixed = RuleAttributeKeyType{valuer.NewString("fixed")}
RuleAttributeTypeLabel = RuleAttributeKeyType{valuer.NewString("label")}
)
type GetRuleAttributeKeys struct {
Key string `json:"key"`
DataType telemetrytypes.FieldDataType `json:"dataType"`
Type RuleAttributeKeyType `json:"type"`
}

View File

@@ -1,31 +1,5 @@
package ruletypes
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
const (
CriticalThresholdName = "CRITICAL"
LabelThresholdName = "threshold.name"
LabelRuleId = "ruleId"
// Rule attribute key constants for search and filtering
RuleAttributeKeyCreatedBy = "created_by"
RuleAttributeKeyUpdatedBy = "updated_by"
RuleAttributeKeyName = "name"
RuleAttributeKeyThresholdName = "threshold.name"
RuleAttributeKeyPolicy = "policy"
RuleAttributeKeyChannel = "channel"
RuleAttributeKeyState = "state"
//RuleAttributeKeyRuleType = "type"
)
var (
FixedRuleAttributeKeys = []GetRuleAttributeKeys{
{Key: RuleAttributeKeyCreatedBy, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyUpdatedBy, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyName, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyThresholdName, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyChannel, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyPolicy, DataType: telemetrytypes.FieldDataTypeBool, Type: RuleAttributeTypeFixed},
{Key: RuleAttributeKeyState, DataType: telemetrytypes.FieldDataTypeString, Type: RuleAttributeTypeFixed},
}
)
const CriticalThresholdName = "CRITICAL"
const LabelThresholdName = "threshold.name"
const LabelRuleId = "ruleId"

View File

@@ -53,11 +53,4 @@ type RuleStore interface {
DeleteRule(context.Context, valuer.UUID, func(context.Context) error) error
GetStoredRules(context.Context, string) ([]*Rule, error)
GetStoredRule(context.Context, valuer.UUID) (*Rule, error)
GetRuleLabelKeys(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetThresholdNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetChannel(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetNames(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetCreatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetUpdatedBy(ctx context.Context, searchText string, limit int, orgId string) ([]string, error)
GetRuleLabelValues(ctx context.Context, searchText string, limit int, labelKey string, orgId string) ([]string, error)
}