Compare commits
24 Commits
chore/filt
...
enhancemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea42ba5ca | ||
|
|
455ba0549f | ||
|
|
5f2c302551 | ||
|
|
15c2dc700a | ||
|
|
02fa0dbc32 | ||
|
|
e0948033c8 | ||
|
|
a1115ac65b | ||
|
|
9bcb88c747 | ||
|
|
367bf7b4b5 | ||
|
|
59b68057b8 | ||
|
|
fa1b2ddf7c | ||
|
|
642a0e5656 | ||
|
|
cb99ee1ac1 | ||
|
|
7616cb89e4 | ||
|
|
bf780c7445 | ||
|
|
61062dfd8d | ||
|
|
5b7af9651c | ||
|
|
b9012f6150 | ||
|
|
7ab81780b3 | ||
|
|
a16f51457f | ||
|
|
38a38b5645 | ||
|
|
bb04bc5044 | ||
|
|
58736f40dc | ||
|
|
91154249d6 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -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
1
.gitignore
vendored
@@ -106,6 +106,7 @@ downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
!frontend/src/lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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}`;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
),
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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), [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -99,11 +99,7 @@ function GridValueComponent({
|
||||
rawValue={value}
|
||||
value={
|
||||
yAxisUnit
|
||||
? getYAxisFormattedValue(
|
||||
String(value),
|
||||
yAxisUnit,
|
||||
widget?.decimalPrecision,
|
||||
)
|
||||
? getYAxisFormattedValue(String(value), yAxisUnit)
|
||||
: value.toString()
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
}),
|
||||
[
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
),
|
||||
[
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
),
|
||||
[
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -158,8 +158,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.log-scale,
|
||||
.decimal-precision-selector {
|
||||
.log-scale {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[]>>;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -249,7 +249,6 @@ function UplotPanelWrapper({
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
decimalPrecision: widget.decimalPrecision,
|
||||
}),
|
||||
[
|
||||
queryResponse.data?.payload,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 };
|
||||
|
||||
634
frontend/src/lib/__tests__/history.test.ts
Normal file
634
frontend/src/lib/__tests__/history.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
21
frontend/src/utils/isEventObject.ts
Normal file
21
frontend/src/utils/isEventObject.ts
Normal 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
|
||||
);
|
||||
};
|
||||
@@ -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==
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user