Compare commits

..

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
5bfd604aa4 ci(jsci): add strict checks for new files only 2026-07-07 11:40:55 -03:00
803 changed files with 13046 additions and 61066 deletions

View File

@@ -48,11 +48,7 @@ jobs:
- logspipelines
- passwordauthn
- preference
- querierlogs
- queriertraces
- queriermetrics
- querierscalar
- queriercommon
- querier
- rawexportdata
- role
- rootuser

View File

@@ -102,3 +102,44 @@ jobs:
run: |
cd frontend && pnpm generate:config:web-settings
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated web settings types. Run pnpm generate:config:web-settings in frontend/ locally and commit."; exit 1)
code-quality:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-pnpm
uses: pnpm/action-setup@v6
with:
version: 10
- name: install-frontend
run: cd frontend && pnpm install
- name: get-new-files
id: new-files
run: |
cd frontend
NEW_TS=$(git diff --name-only --diff-filter=A origin/main...HEAD -- src/ | grep -E '\.(ts|tsx)$' | tr '\n' ' ' || true)
NEW_SCSS=$(git diff --name-only --diff-filter=A origin/main...HEAD -- src/ | grep -E '\.scss$' | tr '\n' ' ' || true)
echo "ts=$NEW_TS" >> $GITHUB_OUTPUT
echo "scss=$NEW_SCSS" >> $GITHUB_OUTPUT
echo "New TS files: $NEW_TS"
echo "New SCSS files: $NEW_SCSS"
- name: lint-new-ts-files
if: steps.new-files.outputs.ts != ''
run: |
cd frontend
pnpm oxlint -c .oxlintrc.strict.json ${{ steps.new-files.outputs.ts }}
- name: lint-new-scss-files
if: steps.new-files.outputs.scss != ''
run: |
cd frontend
pnpm stylelint --config .stylelintrc.strict.cjs ${{ steps.new-files.outputs.scss }}

View File

@@ -129,7 +129,7 @@ sqlstore:
# The timeout for the sqlite database to wait for a lock.
busy_timeout: 10s
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
transaction_mode: immediate
transaction_mode: deferred
##################### APIServer #####################
apiserver:

View File

@@ -1494,7 +1494,7 @@ components:
- cosmosdb
- cassandradb
- redis
- cloudsql_postgres
- cloudsql
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -3148,8 +3148,6 @@ components:
type: string
image:
type: string
legacy:
type: boolean
locked:
type: boolean
name:
@@ -3182,7 +3180,6 @@ components:
- name
- tags
- spec
- legacy
- pinned
type: object
DashboardtypesListedDashboardV2:
@@ -3196,8 +3193,6 @@ components:
type: string
image:
type: string
legacy:
type: boolean
locked:
type: boolean
name:
@@ -3228,7 +3223,6 @@ components:
- name
- tags
- spec
- legacy
type: object
DashboardtypesListedDashboardV2Spec:
properties:
@@ -4195,7 +4189,6 @@ components:
- namespaces
- clusters
- volumes
- kube_containers
type: string
InframonitoringtypesChecks:
properties:
@@ -4301,158 +4294,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesContainerCountsByReady:
properties:
notReady:
type: integer
ready:
type: integer
required:
- ready
- notReady
type: object
InframonitoringtypesContainerCountsByStatus:
properties:
completed:
type: integer
containerCannotRun:
type: integer
containerCreating:
type: integer
crashLoopBackOff:
type: integer
createContainerConfigError:
type: integer
errImagePull:
type: integer
error:
type: integer
imagePullBackOff:
type: integer
oomKilled:
type: integer
running:
type: integer
terminated:
type: integer
unknown:
type: integer
waiting:
type: integer
required:
- running
- waiting
- terminated
- crashLoopBackOff
- imagePullBackOff
- errImagePull
- createContainerConfigError
- containerCreating
- oomKilled
- completed
- error
- containerCannotRun
- unknown
type: object
InframonitoringtypesContainerReady:
enum:
- ready
- not_ready
- no_data
type: string
InframonitoringtypesContainerRecord:
properties:
containerCountsByReady:
$ref: '#/components/schemas/InframonitoringtypesContainerCountsByReady'
containerCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesContainerCountsByStatus'
containerName:
type: string
cpu:
format: double
type: number
cpuLimitUtilization:
format: double
type: number
cpuRequestUtilization:
format: double
type: number
memory:
format: double
type: number
memoryLimitUtilization:
format: double
type: number
memoryRequestUtilization:
format: double
type: number
meta:
additionalProperties:
type: string
nullable: true
type: object
podUID:
type: string
ready:
$ref: '#/components/schemas/InframonitoringtypesContainerReady'
restarts:
format: int64
type: integer
status:
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
required:
- podUID
- containerName
- status
- containerCountsByStatus
- ready
- containerCountsByReady
- restarts
- cpu
- cpuRequestUtilization
- cpuLimitUtilization
- memory
- memoryRequestUtilization
- memoryLimitUtilization
- meta
type: object
InframonitoringtypesContainerStatus:
enum:
- running
- waiting
- terminated
- crashloopbackoff
- imagepullbackoff
- errimagepull
- createcontainerconfigerror
- containercreating
- oomkilled
- completed
- error
- containercannotrun
- unknown
- no_data
type: string
InframonitoringtypesContainers:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesContainerRecord'
type: array
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetRecord:
properties:
currentNodes:
@@ -4484,14 +4325,10 @@ components:
type: string
nullable: true
type: object
misscheduledNodes:
type: integer
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
readyNodes:
type: integer
required:
- daemonSetName
- daemonSetCPU
@@ -4502,8 +4339,6 @@ components:
- daemonSetMemoryLimit
- desiredNodes
- currentNodes
- readyNodes
- misscheduledNodes
- podCountsByPhase
- podCountsByStatus
- meta
@@ -5133,32 +4968,6 @@ components:
- end
- limit
type: object
InframonitoringtypesPostableContainers:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostableDaemonSets:
properties:
end:
@@ -7406,11 +7215,9 @@ components:
op:
$ref: '#/components/schemas/RuletypesCompareOperator'
recoveryTarget:
format: double
nullable: true
type: number
target:
format: double
nullable: true
type: number
targetUnit:
@@ -7694,7 +7501,6 @@ components:
selectedQueryName:
type: string
target:
format: double
nullable: true
type: number
targetUnit:
@@ -7998,15 +7804,6 @@ components:
required:
- items
type: object
SpantypesGettableSpanMappers:
properties:
items:
items:
$ref: '#/components/schemas/SpantypesSpanMapper'
type: array
required:
- items
type: object
SpantypesGettableTraceAggregations:
properties:
aggregations:
@@ -8159,7 +7956,7 @@ components:
type: boolean
fieldContext:
$ref: '#/components/schemas/SpantypesFieldContext'
groupId:
group_id:
type: string
id:
type: string
@@ -8172,7 +7969,7 @@ components:
type: string
required:
- id
- groupId
- group_id
- name
- fieldContext
- config
@@ -13751,7 +13548,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
status:
type: string
required:
@@ -16014,24 +15811,21 @@ paths:
daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row
also reports the latest known node-level counters from kube-state-metrics:
desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the
daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes,
the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes,
the number of nodes running at least one ready daemon pod) and misscheduledNodes
(k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon
pod but not supposed to) — note these are node counts, not pod counts. It
also reports per-group podCountsByPhase ({ pending, running, succeeded, failed,
unknown } from each pod''s latest k8s.pod.phase value). Each daemonset includes
metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name).
The response type is ''list'' for the default k8s.daemonset.name grouping
or ''grouped_list'' for custom groupBy keys; in both modes every row aggregates
pods owned by daemonsets in the group. Supports filtering via a filter expression,
custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
/ memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes,
daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes,
the number of nodes the daemonset currently runs on) — note these are node
counts, not pod counts. It also reports per-group podCountsByPhase ({ pending,
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name,
k8s.cluster.name). The response type is ''list'' for the default k8s.daemonset.name
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
row aggregates pods owned by daemonsets in the group. Supports filtering via
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest,
daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes)
return -1 as a sentinel when no data is available for that field.'
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
when no data is available for that field.'
operationId: ListDaemonSets
requestBody:
content:
@@ -16305,86 +16099,6 @@ paths:
summary: List Jobs for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/kube_containers:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes containers with key kubeletstats
metrics: CPU usage (cores), CPU request/limit utilization, memory working
set, and memory request/limit utilization. Each container also reports health
signals from the k8s_cluster receiver: status (kubectl-style display status
derived from k8s.container.status.state + k8s.container.status.reason), restarts
(absolute count from k8s.container.restarts), and ready (ready/not_ready from
k8s.container.ready). The row identity is (k8s.pod.uid, k8s.container.name),
stable across container restarts. Each container includes metadata attributes
(k8s.container.name, k8s.pod.name, container.image.name, container.image.tag,
k8s.namespace.name, k8s.node.name, k8s.cluster.name, and workload owner such
as deployment/statefulset/daemonset/job). The response type is ''list'' for
the default (k8s.pod.uid, k8s.container.name) grouping (each row is one container
with its current status and ready state) or ''grouped_list'' for custom groupBy
keys (each row aggregates containers in the group with per-status counts under
containerCountsByStatus, per-readiness counts under containerCountsByReady,
and restarts as the group sum). Status requires the optional k8s.container.status.state
and k8s.container.status.reason metrics; when either is missing, status is
omitted and a warning is returned while restarts and ready are still computed.
Supports filtering via a filter expression, custom groupBy, ordering by any
of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit),
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (cpu, cpuRequestUtilization,
cpuLimitUtilization, memory, memoryRequestUtilization, memoryLimitUtilization)
and restarts return -1 as a sentinel when no data is available for that field.'
operationId: ListContainers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableContainers'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesContainers'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Kubernetes Containers for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/namespaces:
post:
deprecated: false
@@ -18195,16 +17909,6 @@ paths:
public dashboard. The panel is addressed by its key in spec.panels.
operationId: GetPublicDashboardPanelQueryRangeV2
parameters:
- in: query
name: startTime
required: false
schema:
type: string
- in: query
name: endTime
required: false
schema:
type: string
- in: path
name: id
required: true

View File

@@ -116,15 +116,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
infraMonitoringV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseInfraMonitoringV2, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureUseInfraMonitoringV2.String()),
Active: infraMonitoringV2,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -3,7 +3,6 @@ package postgressqlschema
import (
"context"
"database/sql"
"log/slog"
"github.com/uptrace/bun"
@@ -215,20 +214,6 @@ WHERE
}
func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
// A key is one entry an index is built on: a column (plain) or an expression
// like lower(x) (functional). Returns one row per key of each non-constraint-
// backed index on the table, ordered by position. Keys are enumerated with
// generate_series over indnkeyatts (count of key columns, excluding non-key
// INCLUDE columns), so expression keys are covered too. Reconstruction uses:
//
// index_name | index the key belongs to
// is_expression | true if the key is an expression, not a plain column
// key_def | rendered key text, e.g. org_id or lower(key)
// column_name | column name for plain keys; NULL for expression keys
// predicate | partial-index WHERE; NULL if not partial (same every row)
//
// table_name/unique/primary/column_position are carried over from the prior
// query (unique gates the loop, column_position orders the keys).
rows, err := provider.
sqlstore.
BunDB().
@@ -239,21 +224,19 @@ SELECT
i.indisunique AS unique,
i.indisprimary AS primary,
a.attname AS column_name,
n AS column_position,
(i.indkey[n - 1] = 0) AS is_expression,
pg_get_indexdef(i.indexrelid, n, true) AS key_def,
array_position(i.indkey, a.attnum) AS column_position,
pg_get_expr(i.indpred, i.indrelid) AS predicate
FROM
pg_index i
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
CROSS JOIN generate_series(1, i.indnkeyatts) AS n
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid AND a.attnum = i.indkey[n - 1]
WHERE
ct.relname = ?
AND ct.relkind = 'r'
a.attnum = ANY(i.indkey)
AND con.oid IS NULL
AND ct.relkind = 'r'
AND ct.relname = ?
ORDER BY index_name, column_position`, string(name))
if err != nil {
return nil, provider.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no indices for table (%s) found", name)
@@ -268,10 +251,6 @@ ORDER BY index_name, column_position`, string(name))
type indexEntry struct {
columns []sqlschema.ColumnName
predicate *string
// keyDefs holds every key rendered by pg_get_indexdef, in key order; used
// for functional indexes where a key may be an expression like lower(x).
keyDefs []string
hasExpression bool
}
uniqueIndicesMap := make(map[string]*indexEntry)
@@ -281,45 +260,30 @@ ORDER BY index_name, column_position`, string(name))
indexName string
unique bool
primary bool
columnName *string
// starts from 1 and is unused in this function, this is to ensure that the column names are in the correct order
columnName string
// starts from 0 and is unused in this function, this is to ensure that the column names are in the correct order
columnPosition int
isExpression bool
keyDef string
predicate *string
)
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition, &isExpression, &keyDef, &predicate); err != nil {
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition, &predicate); err != nil {
return nil, err
}
if !unique {
continue
}
entry, ok := uniqueIndicesMap[indexName]
if !ok {
entry = &indexEntry{predicate: predicate}
uniqueIndicesMap[indexName] = entry
}
entry.keyDefs = append(entry.keyDefs, keyDef)
if isExpression {
entry.hasExpression = true
} else if columnName != nil {
entry.columns = append(entry.columns, sqlschema.ColumnName(*columnName))
if unique {
if _, ok := uniqueIndicesMap[indexName]; !ok {
uniqueIndicesMap[indexName] = &indexEntry{
columns: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
predicate: predicate,
}
} else {
uniqueIndicesMap[indexName].columns = append(uniqueIndicesMap[indexName].columns, sqlschema.ColumnName(columnName))
}
}
}
indices := make([]sqlschema.Index, 0)
for indexName, entry := range uniqueIndicesMap {
// functional partial indexes aren't representable (PartialUniqueIndex has no
// expressions); skip rather than misrepresent.
if entry.hasExpression && entry.predicate != nil {
provider.settings.Logger().WarnContext(ctx, "skipping functional partial unique index; not representable by sqlschema", slog.String("index", indexName), slog.String("table", string(name)))
continue
}
if entry.predicate != nil {
index := &sqlschema.PartialUniqueIndex{
TableName: name,
@@ -327,17 +291,6 @@ ORDER BY index_name, column_position`, string(name))
Where: *entry.predicate,
}
if index.Name() == indexName {
indices = append(indices, index)
} else {
indices = append(indices, index.Named(indexName))
}
} else if entry.hasExpression {
index := &sqlschema.UniqueIndexWithExpressions{
TableName: name,
Expressions: entry.keyDefs,
}
if index.Name() == indexName {
indices = append(indices, index)
} else {

View File

@@ -1,116 +0,0 @@
package postgressqlschema
import (
"context"
"database/sql/driver"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestGetIndices feeds GetIndices the exact rows the catalog query returns for a
// real table (captured from devenv postgres) and checks each unique-index shape
// is reconstructed: expression keys, partial (predicate), and plain columns.
func TestGetIndices(t *testing.T) {
// column order must match the SELECT in GetIndices.
columns := []string{"table_name", "index_name", "unique", "primary", "column_name", "column_position", "is_expression", "key_def", "predicate"}
testCases := []struct {
name string
table sqlschema.TableName
rows [][]driver.Value
want []sqlschema.Index
}{
{
name: "UniqueIndexWithExpressions",
table: "tag",
rows: [][]driver.Value{
{"tag", "uq_tag_a36c51df", true, false, "org_id", 1, false, "org_id", nil},
{"tag", "uq_tag_a36c51df", true, false, "kind", 2, false, "kind", nil},
{"tag", "uq_tag_a36c51df", true, false, nil, 3, true, "lower(key)", nil},
{"tag", "uq_tag_a36c51df", true, false, nil, 4, true, "lower(value)", nil},
},
want: []sqlschema.Index{
&sqlschema.UniqueIndexWithExpressions{
TableName: "tag",
Expressions: []string{"org_id", "kind", "lower(key)", "lower(value)"},
},
},
},
{
name: "PartialUniqueIndexes",
table: "service_account",
rows: [][]driver.Value{
{"service_account", "puq_service_account_email_org_id_471d6134", true, false, "email", 1, false, "email", "(status <> 'deleted'::text)"},
{"service_account", "puq_service_account_email_org_id_471d6134", true, false, "org_id", 2, false, "org_id", "(status <> 'deleted'::text)"},
{"service_account", "puq_service_account_name_org_id_471d6134", true, false, "name", 1, false, "name", "(status <> 'deleted'::text)"},
{"service_account", "puq_service_account_name_org_id_471d6134", true, false, "org_id", 2, false, "org_id", "(status <> 'deleted'::text)"},
},
want: []sqlschema.Index{
(&sqlschema.PartialUniqueIndex{
TableName: "service_account",
ColumnNames: []sqlschema.ColumnName{"email", "org_id"},
Where: "(status <> 'deleted'::text)",
}).Named("puq_service_account_email_org_id_471d6134"),
(&sqlschema.PartialUniqueIndex{
TableName: "service_account",
ColumnNames: []sqlschema.ColumnName{"name", "org_id"},
Where: "(status <> 'deleted'::text)",
}).Named("puq_service_account_name_org_id_471d6134"),
},
},
{
name: "PlainUniqueIndex",
table: "tag_relation",
rows: [][]driver.Value{
{"tag_relation", "uq_tag_relation_kind_resource_id_tag_id", true, false, "kind", 1, false, "kind", nil},
{"tag_relation", "uq_tag_relation_kind_resource_id_tag_id", true, false, "resource_id", 2, false, "resource_id", nil},
{"tag_relation", "uq_tag_relation_kind_resource_id_tag_id", true, false, "tag_id", 3, false, "tag_id", nil},
},
want: []sqlschema.Index{
&sqlschema.UniqueIndex{
TableName: "tag_relation",
ColumnNames: []sqlschema.ColumnName{"kind", "resource_id", "tag_id"},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
store := sqlstoretest.New(sqlstore.Config{Provider: "postgres"}, sqlmock.QueryMatcherRegexp)
rows := sqlmock.NewRows(columns)
for _, row := range testCase.rows {
rows.AddRow(row...)
}
store.Mock().ExpectQuery("pg_index").WillReturnRows(rows)
schema, err := New(context.Background(), instrumentationtest.New().ToProviderSettings(), sqlschema.Config{}, store)
require.NoError(t, err)
indices, err := schema.GetIndices(context.Background(), testCase.table)
require.NoError(t, err)
assert.Len(t, indices, len(testCase.want))
// GetIndices iterates a map, so match by name rather than slice order.
got := make(map[string]sqlschema.Index, len(indices))
for _, index := range indices {
got[index.Name()] = index
}
for _, want := range testCase.want {
actual, ok := got[want.Name()]
if !assert.True(t, ok, "expected index %s not returned", want.Name()) {
// so that other items in the for loop can also be checked.
continue
}
assert.True(t, want.Equals(actual), "index %s reconstructed differently", want.Name())
}
})
}
}

View File

@@ -295,6 +295,18 @@
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
"signoz/no-dashboard-fetch-outside-root": "error",
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
"signoz/no-raw-html-text-elements": "warn",
// Bans raw span/p/h1-6 elements - use Typography component instead
"signoz/no-multiple-components": "warn",
// Bans multiple component exports per file - split into separate files
"signoz/no-inline-helpers": "warn",
// Bans too many helper functions in component files - move to utils.ts
"signoz/max-component-lines": ["warn", { "max": 300 }],
// Warns when component files exceed max lines - split into smaller components
"signoz/no-getByText": "warn",
// Bans getByText in tests - use getByTestId or getByRole instead
"signoz/no-fireEvent": "warn",
// Bans fireEvent in tests - use userEvent instead
"no-restricted-globals": [
"error",
{

View File

@@ -0,0 +1,18 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["./.oxlintrc.json"],
"env": {
"builtin": true,
"es2021": true,
"browser": true,
"jest": true,
"node": true
},
"rules": {
"signoz/no-raw-html-text-elements": "error",
"signoz/no-multiple-components": "error",
"signoz/no-inline-helpers": "error",
"signoz/no-getByText": "error",
"signoz/no-fireEvent": "error"
}
}

View File

@@ -12,11 +12,17 @@ module.exports = {
),
path.join(__dirname, 'stylelint-rules/css-modules/prefer-css-variables.js'),
path.join(__dirname, 'stylelint-rules/css-modules/class-name-pattern.js'),
path.join(__dirname, 'stylelint-rules/no-font-properties.js'),
path.join(__dirname, 'stylelint-rules/no-primitive-color-tokens.js'),
path.join(__dirname, 'stylelint-rules/no-styles-scss.js'),
],
customSyntax: 'postcss-scss',
rules: {
// Applies to all SCSS files
'local/no-unsupported-asset-url': true,
'local/no-font-properties': [true, { severity: 'warning' }],
'local/no-primitive-color-tokens': [true, { severity: 'warning' }],
'local/no-styles-scss': [true, { severity: 'warning' }],
},
overrides: [
{

View File

@@ -0,0 +1,14 @@
// Strict config for new files - rules that are warnings in base config become errors here
// oxlint-disable-next-line typescript/no-require-imports
const baseConfig = require('./.stylelintrc.cjs');
module.exports = {
...baseConfig,
rules: {
...baseConfig.rules,
// Override warnings to errors for new files
'local/no-font-properties': true, // error (no severity = error)
'local/no-primitive-color-tokens': true,
'local/no-styles-scss': true,
},
};

View File

@@ -1,88 +0,0 @@
// oxlint-disable-next-line no-restricted-imports
import * as React from 'react';
// In jsdom, AnimatePresence from motion/react keeps children in DOM during exit
// animations (awaiting rAF-driven completion that never fully runs in jsdom).
// This mock makes AnimatePresence render children immediately and makes motion.*
// elements render as their plain HTML equivalents without animation side-effects.
//
// IMPORTANT: motion component references are cached so React sees a stable
// component identity across re-renders and does not enter an infinite remount loop.
const MOTION_PROPS_TO_STRIP = new Set([
'initial',
'animate',
'exit',
'variants',
'transition',
'whileHover',
'whileTap',
'whileFocus',
'whileInView',
'layout',
'layoutId',
'onAnimationStart',
'onAnimationComplete',
]);
const cache = new Map<string, React.ComponentType>();
function getMotionComponent(tag: string): React.ComponentType {
if (!cache.has(tag)) {
const Component = React.forwardRef<HTMLElement, Record<string, unknown>>(
(props, ref) => {
const domProps: Record<string, unknown> = {};
for (const [k, v] of Object.entries(props)) {
if (!MOTION_PROPS_TO_STRIP.has(k)) {
domProps[k] = v;
}
}
return React.createElement(tag, { ...domProps, ref });
},
);
Component.displayName = `motion.${tag}`;
cache.set(tag, Component as unknown as React.ComponentType);
}
return cache.get(tag) as React.ComponentType;
}
const motionHandler: ProxyHandler<Record<string, React.ComponentType>> = {
get(_target, prop: string) {
return getMotionComponent(prop);
},
};
export const AnimatePresence: React.FC<{
children?: React.ReactNode;
mode?: string;
}> = ({ children }) => React.createElement(React.Fragment, null, children);
export const motion = new Proxy(
{} as Record<string, React.ComponentType>,
motionHandler,
);
export const useAnimation = (): Record<string, unknown> => ({
start: (): unknown => Promise.resolve(),
stop: (): unknown => undefined,
set: (): unknown => undefined,
});
export const useMotionValue = (
initial: unknown,
): { get: () => unknown; set: () => void } => ({
get: (): unknown => initial,
set: (): unknown => undefined,
});
export const useTransform = (): { get: () => number } => ({
get: (): number => 0,
});
export const useSpring = (v: unknown): unknown => v;
export const useScroll = (): { scrollY: { get: () => number } } => ({
scrollY: { get: (): number => 0 },
});
export default { motion, AnimatePresence };

View File

@@ -20,7 +20,6 @@ const config: Config.InitialOptions = {
'\\.module\\.mjs$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^motion/react$': '<rootDir>/__mocks__/motionMock.tsx',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,

View File

@@ -16,6 +16,8 @@
"lint:generated": "oxlint ./src/api/generated --fix",
"lint:fix": "oxlint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"lint:strict": "oxlint -c .oxlintrc.strict.json",
"lint:styles:strict": "stylelint --config .stylelintrc.strict.cjs",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
@@ -75,11 +77,10 @@
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.11",
"dompurify": "3.4.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
"html-to-image": "1.11.13",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
@@ -238,18 +239,10 @@
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"brace-expansion": "^2.0.3",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"js-cookie": "^3.0.7",
"tmp": "0.2.7",
"vite": "npm:rolldown-vite@7.3.1",
"dompurify": "3.4.11",
"js-yaml@3": "3.15.0",
"js-yaml@4": "4.2.0",
"yaml@1": "1.10.3",
"react-router@6": "6.30.4",
"markdown-it": "14.2.0",
"mdast-util-to-hast@13": "13.2.1",
"protocol-buffers-schema": "3.6.1"
"vite": "npm:rolldown-vite@7.3.1"
}
}

View File

@@ -0,0 +1,71 @@
/**
* Rule: max-component-lines
*
* Warns when a component file exceeds configured line limit.
* Large components should be split into smaller ones.
*
* Config: ["warn", { "max": 300 }]
* Default: 300 lines
*
* This rule should always be "warn" (never "error").
*/
const DEFAULT_MAX_LINES = 300;
export default {
meta: {
docs: {
description: 'Warn when component files exceed a line limit',
category: 'Code Quality',
},
schema: [
{
type: 'object',
properties: {
max: {
type: 'number',
minimum: 1,
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options?.[0] || {};
const maxLines = options.max || DEFAULT_MAX_LINES;
const filename = context.getFilename();
// Only check .tsx files (component files)
if (!/\.tsx$/.test(filename)) {
return {};
}
// Skip test files
if (/\.(test|spec)\.tsx$/.test(filename)) {
return {};
}
// Skip utility/type files
if (
/\/(utils|helpers|constants|types|hooks)\.(ts|tsx)$/.test(filename) ||
/\/(utils|helpers|constants|types)\//.test(filename)
) {
return {};
}
return {
Program(node) {
const sourceCode = context.getSourceCode();
const lines = sourceCode.lines?.length || sourceCode.getText().split('\n').length;
if (lines > maxLines) {
context.report({
node,
message: `Component file has ${lines} lines (max ${maxLines}). Consider splitting into smaller components or extracting logic into hooks/utils.`,
});
}
},
};
},
};

View File

@@ -0,0 +1,48 @@
/**
* Rule: no-fireEvent
*
* Bans fireEvent in test files. Use userEvent instead for more realistic
* user interaction simulation.
*
* BAD:
* fireEvent.click(button)
* fireEvent.change(input, { target: { value: 'test' } })
*
* GOOD:
* await userEvent.click(button)
* await userEvent.type(input, 'test')
*/
export default {
create(context) {
return {
// fireEvent.click(...), fireEvent.change(...)
MemberExpression(node) {
if (
node.object.type === 'Identifier' &&
node.object.name === 'fireEvent'
) {
context.report({
node,
message:
"Avoid 'fireEvent'. Use userEvent from @testing-library/user-event for more realistic user interactions.",
});
}
},
// import { fireEvent } from '@testing-library/react'
ImportSpecifier(node) {
if (
node.imported.type === 'Identifier' &&
node.imported.name === 'fireEvent'
) {
context.report({
node,
message:
"Avoid importing 'fireEvent'. Use userEvent from @testing-library/user-event instead.",
});
}
},
};
},
};

View File

@@ -0,0 +1,53 @@
/**
* Rule: no-getByText
*
* Bans getByText, queryByText, findByText, getAllByText, queryAllByText, findAllByText
* in test files. Use getByTestId or getByRole instead.
*
* BAD:
* screen.getByText('Submit')
* getByText('Submit')
*
* GOOD:
* screen.getByTestId('submit-button')
* screen.getByRole('button', { name: 'Submit' })
*/
const BANNED_METHODS = new Set([
'getByText',
'queryByText',
'findByText',
'getAllByText',
'queryAllByText',
'findAllByText',
]);
export default {
create(context) {
return {
CallExpression(node) {
let methodName = null;
// screen.getByText(...) or result.getByText(...)
if (
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier'
) {
methodName = node.callee.property.name;
}
// Direct getByText(...) from destructured import
if (node.callee.type === 'Identifier') {
methodName = node.callee.name;
}
if (methodName && BANNED_METHODS.has(methodName)) {
context.report({
node,
message: `Avoid '${methodName}'. Use getByTestId or getByRole instead for more stable selectors.`,
});
}
},
};
},
};

View File

@@ -0,0 +1,161 @@
/**
* Rule: no-inline-helpers
*
* Bans too many helper functions in component files.
* Helper functions should be moved to utils.ts or specialized files.
*
* Exempt:
* - Hooks (use* functions)
* - Exported functions (they're intentionally in the file)
* - Files named utils.ts, helpers.ts, constants.ts
*
* Threshold: More than 3 private helper functions triggers warning.
*/
const MAX_HELPERS = 3;
function isHookName(name) {
return /^use[A-Z]/.test(name);
}
function isComponentName(name) {
return /^[A-Z]/.test(name);
}
export default {
create(context) {
const filename = context.getFilename();
// Skip utility files
if (
/\/(utils|helpers|constants|types|hooks)\.(ts|tsx|js|jsx)$/.test(filename) ||
/\/(utils|helpers|constants|types)\//.test(filename)
) {
return {};
}
// Skip test files
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename)) {
return {};
}
const privateHelpers = [];
const exportedNames = new Set();
let hasComponent = false;
return {
// Track exported names
ExportNamedDeclaration(node) {
if (node.declaration) {
if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
exportedNames.add(node.declaration.id.name);
if (isComponentName(node.declaration.id.name)) {
hasComponent = true;
}
}
if (node.declaration.type === 'VariableDeclaration') {
for (const decl of node.declaration.declarations) {
if (decl.id.type === 'Identifier') {
exportedNames.add(decl.id.name);
if (isComponentName(decl.id.name)) {
hasComponent = true;
}
}
}
}
}
},
ExportDefaultDeclaration(node) {
if (
node.declaration.type === 'FunctionDeclaration' &&
node.declaration.id &&
isComponentName(node.declaration.id.name)
) {
hasComponent = true;
}
if (node.declaration.type === 'Identifier') {
exportedNames.add(node.declaration.name);
}
},
// Track private function declarations
FunctionDeclaration(node) {
if (!node.id) {
return;
}
const name = node.id.name;
// Will check if exported later
if (
!isHookName(name) &&
!isComponentName(name) &&
node.parent.type !== 'ExportNamedDeclaration' &&
node.parent.type !== 'ExportDefaultDeclaration'
) {
privateHelpers.push({ name, node });
}
},
// Track private arrow/function expressions
VariableDeclaration(node) {
// Skip if this is an export
if (
node.parent.type === 'ExportNamedDeclaration' ||
node.parent.type === 'Program'
) {
for (const decl of node.declarations) {
if (decl.id.type !== 'Identifier') {
continue;
}
const name = decl.id.name;
const init = decl.init;
// Only track functions
if (
!init ||
(init.type !== 'ArrowFunctionExpression' &&
init.type !== 'FunctionExpression')
) {
continue;
}
// Skip hooks, components, and exports
if (
!isHookName(name) &&
!isComponentName(name) &&
node.parent.type !== 'ExportNamedDeclaration'
) {
privateHelpers.push({ name, node: decl });
}
}
}
},
'Program:exit'() {
// Only check files with components
if (!hasComponent) {
return;
}
// Filter out any that ended up exported
const actualPrivate = privateHelpers.filter(
(h) => !exportedNames.has(h.name),
);
if (actualPrivate.length > MAX_HELPERS) {
// Report on helpers beyond the threshold
for (let i = MAX_HELPERS; i < actualPrivate.length; i++) {
const { name, node } = actualPrivate[i];
context.report({
node,
message: `Too many helper functions (${actualPrivate.length}) in component file. Move '${name}' and other helpers to utils.ts. Helpers: ${actualPrivate.map((h) => h.name).join(', ')}.`,
});
}
}
},
};
},
};

View File

@@ -0,0 +1,142 @@
/**
* Rule: no-multiple-components
*
* Bans multiple React component exports in a single file.
* Each component should have its own file.
*
* BAD:
* export function ComponentA() { return <div /> }
* export function ComponentB() { return <div /> }
*
* GOOD:
* // ComponentA.tsx
* export function ComponentA() { return <div /> }
*
* // ComponentB.tsx (separate file)
* export function ComponentB() { return <div /> }
*
* Note: index.tsx files are exempt (re-export barrels).
*/
function isPascalCase(name) {
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
}
function hasJSXReturn(node) {
if (!node.body) {
return false;
}
// Arrow function with direct JSX return: () => <div />
if (node.body.type === 'JSXElement' || node.body.type === 'JSXFragment') {
return true;
}
// Function body - check return statements
if (node.body.type === 'BlockStatement') {
for (const stmt of node.body.body) {
if (
stmt.type === 'ReturnStatement' &&
stmt.argument &&
(stmt.argument.type === 'JSXElement' ||
stmt.argument.type === 'JSXFragment' ||
// Conditional: condition ? <A /> : <B />
(stmt.argument.type === 'ConditionalExpression' &&
(stmt.argument.consequent.type === 'JSXElement' ||
stmt.argument.alternate.type === 'JSXElement')) ||
// Logical: condition && <A />
(stmt.argument.type === 'LogicalExpression' &&
(stmt.argument.left.type === 'JSXElement' ||
stmt.argument.right.type === 'JSXElement')))
) {
return true;
}
}
}
return false;
}
export default {
create(context) {
const exportedComponents = [];
return {
// export function ComponentName() { }
ExportNamedDeclaration(node) {
if (!node.declaration) {
return;
}
// function ComponentName() {}
if (node.declaration.type === 'FunctionDeclaration') {
const name = node.declaration.id?.name;
if (name && isPascalCase(name) && hasJSXReturn(node.declaration)) {
exportedComponents.push({ name, node });
}
}
// const ComponentName = () => {} or const ComponentName = function() {}
if (node.declaration.type === 'VariableDeclaration') {
for (const declarator of node.declaration.declarations) {
const name = declarator.id?.name;
if (!name || !isPascalCase(name)) {
continue;
}
const init = declarator.init;
if (!init) {
continue;
}
// Arrow function or function expression
if (
(init.type === 'ArrowFunctionExpression' ||
init.type === 'FunctionExpression') &&
hasJSXReturn(init)
) {
exportedComponents.push({ name, node });
}
// React.memo(Component) or memo(Component)
if (
init.type === 'CallExpression' &&
((init.callee.type === 'Identifier' &&
(init.callee.name === 'memo' ||
init.callee.name === 'forwardRef')) ||
(init.callee.type === 'MemberExpression' &&
init.callee.object.name === 'React' &&
(init.callee.property.name === 'memo' ||
init.callee.property.name === 'forwardRef')))
) {
exportedComponents.push({ name, node });
}
}
}
},
// export default function ComponentName() { }
ExportDefaultDeclaration(node) {
if (node.declaration.type === 'FunctionDeclaration') {
const name = node.declaration.id?.name;
if (name && isPascalCase(name) && hasJSXReturn(node.declaration)) {
exportedComponents.push({ name, node });
}
}
},
'Program:exit'() {
if (exportedComponents.length > 1) {
// Report on all but the first component
for (let i = 1; i < exportedComponents.length; i++) {
const { name, node } = exportedComponents[i];
context.report({
node,
message: `Multiple component exports in same file. Move '${name}' to its own file. Found ${exportedComponents.length} components: ${exportedComponents.map((c) => c.name).join(', ')}.`,
});
}
}
},
};
},
};

View File

@@ -0,0 +1,39 @@
/**
* Rule: no-raw-html-text-elements
*
* Bans raw HTML text elements (span, p, h1-h6) in JSX.
* Use Typography component from @signozhq/ui instead.
*
* BAD:
* <span>Hello</span>
* <p>Paragraph</p>
* <h1>Title</h1>
*
* GOOD:
* <Typography>Hello</Typography>
* <Typography.Text>Hello</Typography.Text>
* <Typography.Title level={1}>Title</Typography.Title>
*/
const BANNED_ELEMENTS = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
export default {
create(context) {
return {
JSXOpeningElement(node) {
if (node.name.type !== 'JSXIdentifier') {
return;
}
const elementName = node.name.name;
if (BANNED_ELEMENTS.has(elementName)) {
context.report({
node,
message: `Avoid raw '<${elementName}>' element. Use Typography component from @signozhq/ui instead.`,
});
}
},
};
},
};

View File

@@ -13,6 +13,12 @@ import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
import noGetByText from './rules/no-getByText.mjs';
import noFireEvent from './rules/no-fireEvent.mjs';
import noRawHtmlTextElements from './rules/no-raw-html-text-elements.mjs';
import noMultipleComponents from './rules/no-multiple-components.mjs';
import noInlineHelpers from './rules/no-inline-helpers.mjs';
import maxComponentLines from './rules/max-component-lines.mjs';
export default {
meta: {
@@ -27,5 +33,11 @@ export default {
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
'no-getByText': noGetByText,
'no-fireEvent': noFireEvent,
'no-raw-html-text-elements': noRawHtmlTextElements,
'no-multiple-components': noMultipleComponents,
'no-inline-helpers': noInlineHelpers,
'max-component-lines': maxComponentLines,
},
};

160
frontend/pnpm-lock.yaml generated
View File

@@ -19,19 +19,11 @@ overrides:
prismjs: 1.30.0
got: 11.8.5
form-data: 4.0.6
brace-expansion: ^2.0.3
brace-expansion: ^2.0.2
on-headers: ^1.1.0
js-cookie: ^3.0.7
tmp: 0.2.7
vite: npm:rolldown-vite@7.3.1
dompurify: 3.4.11
js-yaml@3: 3.15.0
js-yaml@4: 4.2.0
yaml@1: 1.10.3
react-router@6: 6.30.4
markdown-it: 14.2.0
mdast-util-to-hast@13: 13.2.1
protocol-buffers-schema: 3.6.1
importers:
@@ -87,7 +79,7 @@ importers:
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/ui':
specifier: 0.0.23
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -161,8 +153,8 @@ importers:
specifier: ^1.10.7
version: 1.11.20
dompurify:
specifier: 3.4.11
version: 3.4.11
specifier: 3.4.0
version: 3.4.0
event-source-polyfill:
specifier: 1.0.31
version: 1.0.31
@@ -172,9 +164,6 @@ importers:
history:
specifier: 4.10.1
version: 4.10.1
html-to-image:
specifier: 1.11.13
version: 1.11.13
http-status-codes:
specifier: 2.3.0
version: 2.3.0
@@ -204,7 +193,7 @@ importers:
version: 12.4.13(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nuqs:
specifier: 2.8.8
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
overlayscrollbars:
specifier: ^2.8.1
version: 2.9.2
@@ -3048,10 +3037,6 @@ packages:
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
engines: {node: '>=14.0.0'}
'@remix-run/router@1.23.3':
resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==}
engines: {node: '>=14.0.0'}
'@rolldown/binding-android-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -4155,8 +4140,8 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
brace-expansion@2.1.1:
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -4817,8 +4802,14 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.11:
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
dompurify@3.2.4:
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
dompurify@3.2.7:
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
dompurify@3.4.0:
resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -5460,9 +5451,6 @@ packages:
resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
engines: {node: '>=20.10'}
html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -6061,12 +6049,12 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsdom@20.0.3:
@@ -6250,8 +6238,8 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.2:
resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lint-staged@17.0.4:
resolution: {integrity: sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA==}
@@ -6370,8 +6358,8 @@ packages:
mapbox-to-css-font@2.4.5:
resolution: {integrity: sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==}
markdown-it@14.2.0:
resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==}
markdown-it@14.1.1:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
markdown-table@3.0.4:
@@ -6435,8 +6423,8 @@ packages:
mdast-util-to-hast@12.3.0:
resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==}
mdast-util-to-hast@13.2.1:
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
mdast-util-to-hast@13.1.0:
resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==}
mdast-util-to-markdown@1.5.0:
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
@@ -6778,7 +6766,7 @@ packages:
'@tanstack/react-router': ^1
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: 6.30.4
react-router: ^5 || ^6 || ^7
react-router-dom: ^5 || ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
@@ -6799,7 +6787,7 @@ packages:
'@tanstack/react-router': ^1
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: 6.30.4
react-router: ^5 || ^6 || ^7
react-router-dom: ^5 || ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
@@ -7180,8 +7168,8 @@ packages:
property-information@6.3.0:
resolution: {integrity: sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==}
protocol-buffers-schema@3.6.1:
resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -7696,8 +7684,8 @@ packages:
peerDependencies:
react: '>=15'
react-router@6.30.4:
resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==}
react-router@6.30.3:
resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
@@ -8952,8 +8940,8 @@ packages:
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@1.10.3:
resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
yaml@2.8.2:
@@ -9074,7 +9062,7 @@ snapshots:
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.2.0
js-yaml: 4.1.1
'@babel/code-frame@7.29.0':
dependencies:
@@ -10333,7 +10321,7 @@ snapshots:
'@types/string-hash': 1.1.3
d3-interpolate: 3.0.1
date-fns: 4.1.0
dompurify: 3.4.11
dompurify: 3.2.4
eventemitter3: 5.0.1
fast_array_intersect: 1.1.0
history: 4.10.1
@@ -10484,7 +10472,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.15.0
js-yaml: 3.14.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.3': {}
@@ -11811,8 +11799,6 @@ snapshots:
'@remix-run/router@1.23.2': {}
'@remix-run/router@1.23.3': {}
'@rolldown/binding-android-arm64@1.0.0-beta.53':
optional: true
@@ -12050,7 +12036,7 @@ snapshots:
- react-dom
- tailwindcss
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
dependencies:
'@chenglou/pretext': 0.0.5
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -12079,7 +12065,7 @@ snapshots:
lodash-es: 4.18.1
motion: 11.18.2(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nuqs: 2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
nuqs: 2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-day-picker: 9.9.0(react@18.2.0)
react-dom: 18.2.0(react@18.2.0)
@@ -13065,7 +13051,7 @@ snapshots:
boolbase@1.0.0: {}
brace-expansion@2.1.1:
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
@@ -13389,14 +13375,14 @@ snapshots:
import-fresh: 3.3.1
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.3
yaml: 1.10.2
optional: true
cosmiconfig@9.0.1(typescript@5.9.3):
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.2.0
js-yaml: 4.1.1
parse-json: 5.2.0
optionalDependencies:
typescript: 5.9.3
@@ -13728,7 +13714,15 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.11:
dompurify@3.2.4:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.2.7:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.4.0:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -14416,7 +14410,7 @@ snapshots:
hast-util-from-parse5: 8.0.1
hast-util-to-parse5: 8.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.1
mdast-util-to-hast: 13.1.0
parse5: 7.3.0
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
@@ -14501,8 +14495,6 @@ snapshots:
html-tags@5.1.0: {}
html-to-image@1.11.13: {}
html-void-elements@3.0.0: {}
http-proxy-agent@5.0.0:
@@ -15342,12 +15334,12 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.0:
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.2.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -15396,7 +15388,7 @@ snapshots:
'@types/json-schema': 7.0.15
'@types/lodash': 4.17.24
is-glob: 4.0.3
js-yaml: 4.2.0
js-yaml: 4.1.1
lodash: 4.18.1
minimist: 1.2.8
prettier: 3.8.3
@@ -15533,7 +15525,7 @@ snapshots:
lines-and-columns@1.2.4: {}
linkify-it@5.0.2:
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
@@ -15654,11 +15646,11 @@ snapshots:
mapbox-to-css-font@2.4.5: {}
markdown-it@14.2.0:
markdown-it@14.1.1:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.2
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
@@ -15772,7 +15764,7 @@ snapshots:
unist-util-position: 4.0.4
unist-util-visit: 4.1.2
mdast-util-to-hast@13.2.1:
mdast-util-to-hast@13.1.0:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.3
@@ -16049,19 +16041,19 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 2.1.1
brace-expansion: 2.0.2
minimatch@3.1.5:
dependencies:
brace-expansion: 2.1.1
brace-expansion: 2.0.2
minimatch@5.1.9:
dependencies:
brace-expansion: 2.1.1
brace-expansion: 2.0.2
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.1
brace-expansion: 2.0.2
minimist@1.2.8: {}
@@ -16075,7 +16067,7 @@ snapshots:
monaco-editor@0.55.1:
dependencies:
dompurify: 3.4.11
dompurify: 3.2.7
marked: 14.0.0
motion-dom@11.18.1:
@@ -16216,20 +16208,20 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0):
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0):
dependencies:
'@standard-schema/spec': 1.0.0
react: 18.2.0
optionalDependencies:
react-router: 6.30.4(react@18.2.0)
react-router: 6.30.3(react@18.2.0)
react-router-dom: 5.3.4(react@18.2.0)
nuqs@2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0):
nuqs@2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0):
dependencies:
'@standard-schema/spec': 1.0.0
react: 18.2.0
optionalDependencies:
react-router: 6.30.4(react@18.2.0)
react-router: 6.30.3(react@18.2.0)
react-router-dom: 5.3.4(react@18.2.0)
nwsapi@2.2.23: {}
@@ -16336,7 +16328,7 @@ snapshots:
find-up: 8.0.0
fs-extra: 11.3.3
jiti: 2.6.1
js-yaml: 4.2.0
js-yaml: 4.1.1
remeda: 2.34.0
string-argv: 0.3.2
tsconfck: 3.1.6(typescript@5.9.3)
@@ -16549,7 +16541,7 @@ snapshots:
postcss-load-config@3.1.4(postcss@8.5.14)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3)):
dependencies:
lilconfig: 2.1.0
yaml: 1.10.3
yaml: 1.10.2
optionalDependencies:
postcss: 8.5.14
ts-node: 10.9.1(@types/node@16.18.25)(typescript@5.9.3)
@@ -16654,7 +16646,7 @@ snapshots:
property-information@6.3.0: {}
protocol-buffers-schema@3.6.1: {}
protocol-buffers-schema@3.6.0: {}
proxy-from-env@1.1.0: {}
@@ -17264,7 +17256,7 @@ snapshots:
history: 5.3.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-router: 6.30.4(react@18.2.0)
react-router: 6.30.3(react@18.2.0)
react-router-dom: 5.3.4(react@18.2.0)
react-router-dom@5.3.4(react@18.2.0):
@@ -17291,9 +17283,9 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
react-router@6.30.4(react@18.2.0):
react-router@6.30.3(react@18.2.0):
dependencies:
'@remix-run/router': 1.23.3
'@remix-run/router': 1.23.2
react: 18.2.0
react-style-singleton@2.2.3(@types/react@18.0.26)(react@18.2.0):
@@ -17517,7 +17509,7 @@ snapshots:
resolve-protobuf-schema@2.1.0:
dependencies:
protocol-buffers-schema: 3.6.1
protocol-buffers-schema: 3.6.0
resolve@1.22.11:
dependencies:
@@ -18209,7 +18201,7 @@ snapshots:
dependencies:
'@gerrit0/mini-shiki': 3.23.0
lunr: 2.3.9
markdown-it: 14.2.0
markdown-it: 14.1.1
minimatch: 10.2.5
typescript: 5.9.3
yaml: 2.8.4
@@ -18673,7 +18665,7 @@ snapshots:
yallist@4.0.0: {}
yaml@1.10.3: {}
yaml@1.10.2: {}
yaml@2.8.2: {}

View File

@@ -62,6 +62,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
}
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
}

View File

@@ -87,6 +87,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
}
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
}

View File

@@ -329,3 +329,10 @@ export const LLMObservabilityPage = Loadable(
/* webpackChunkName: "LLM Observability Page" */ 'pages/LLMObservability'
),
);
export const LLMObservabilityModelPricingPage = Loadable(
() =>
import(
/* webpackChunkName: "LLM Observability Model Pricing Page" */ 'pages/LLMObservabilityModelPricing'
),
);

View File

@@ -24,6 +24,7 @@ import {
LicensePage,
ListAllALertsPage,
LLMObservabilityPage,
LLMObservabilityModelPricingPage,
LiveLogs,
Login,
Logs,
@@ -514,24 +515,17 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
path: ROUTES.LLM_OBSERVABILITY_BASE,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_ATTRIBUTE_MAPPING',
key: 'LLM_OBSERVABILITY_BASE',
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
path: ROUTES.LLM_OBSERVABILITY_MODEL_PRICING,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_CONFIGURATION',
component: LLMObservabilityModelPricingPage,
key: 'LLM_OBSERVABILITY_MODEL_PRICING',
isPrivate: true,
},
];

View File

@@ -269,170 +269,3 @@ export function mockQueryRangeV5WithEventsResponse({
),
);
}
export type MockTracesOptions = {
offset?: number;
pageSize?: number;
hasMore?: boolean;
delay?: number;
customTraces?: Array<{
serviceName?: string;
name?: string;
durationNano?: number;
httpMethod?: string;
responseStatusCode?: string;
}>;
onReceiveRequest?: (
req: RestRequest,
) =>
| undefined
| void
| Omit<MockTracesOptions, 'onReceiveRequest'>
| Promise<Omit<MockTracesOptions, 'onReceiveRequest'>>
| Promise<void>;
};
const createTracesResponse = ({
offset = 0,
pageSize = 10,
hasMore = true,
customTraces,
}: MockTracesOptions): MetricRangePayloadV5 => {
const serviceNames = ['frontend', 'backend', 'database', 'api-gateway'];
const spanNames = [
'GET /api/users',
'POST /api/orders',
'SELECT * FROM users',
];
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE'];
const statusCodes = ['200', '201', '400', '404', '500'];
const rows = customTraces
? customTraces.map((trace, index) => {
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
const currentTimestamp = new Date(baseTimestamp - index * 60000);
return {
timestamp: currentTimestamp.toISOString(),
data: {
serviceName:
trace.serviceName ?? serviceNames[index % serviceNames.length],
name: trace.name ?? spanNames[index % spanNames.length],
durationNano: trace.durationNano ?? 1000000 + index * 100000,
httpMethod: trace.httpMethod ?? httpMethods[index % httpMethods.length],
responseStatusCode:
trace.responseStatusCode ?? statusCodes[index % statusCodes.length],
traceID: `trace-id-${index}`,
spanID: `span-id-${index}`,
},
};
})
: Array.from(
{ length: hasMore ? pageSize : Math.ceil(pageSize / 2) },
(_, index) => {
const cumulativeIndex = offset + index;
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
const currentTimestamp = new Date(baseTimestamp - cumulativeIndex * 60000);
return {
timestamp: currentTimestamp.toISOString(),
data: {
serviceName: serviceNames[cumulativeIndex % serviceNames.length],
name: spanNames[cumulativeIndex % spanNames.length],
durationNano: 1000000 + cumulativeIndex * 100000,
httpMethod: httpMethods[cumulativeIndex % httpMethods.length],
responseStatusCode: statusCodes[cumulativeIndex % statusCodes.length],
traceID: `trace-id-${cumulativeIndex}`,
spanID: `span-id-${cumulativeIndex}`,
},
};
},
);
return {
data: {
type: 'raw',
data: {
results: [
{
queryName: 'A',
nextCursor: hasMore ? 'next-cursor-token' : '',
rows,
},
],
},
meta: {
bytesScanned: 9682976,
durationMs: 295,
rowsScanned: 34198,
stepIntervals: { A: 170 },
},
},
};
};
export function mockQueryRangeV5WithTracesResponse({
hasMore = true,
offset = 0,
pageSize = 10,
delay = 0,
customTraces,
onReceiveRequest,
}: MockTracesOptions = {}): void {
server.use(
rest.post(QUERY_RANGE_URL, async (req, res, ctx) =>
res(
...(delay ? [ctx.delay(delay)] : []),
ctx.status(200),
ctx.json(
createTracesResponse(
(await onReceiveRequest?.(req)) ?? {
hasMore,
pageSize,
offset,
customTraces,
},
),
),
),
),
);
}
export function mockQueryRangeV5WithEmptyTraces(): void {
server.use(
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: {
type: 'raw',
data: {
results: [{ queryName: 'A', nextCursor: '', rows: [] }],
},
meta: {
bytesScanned: 0,
durationMs: 10,
rowsScanned: 0,
stepIntervals: {},
},
},
}),
),
),
);
}
export function mockQueryRangeV5WithKeyNotFoundError(): void {
server.use(
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
res(
ctx.status(400),
ctx.json({
error: {
code: 'invalid_input',
errors: [{ message: 'key not found' }],
},
}),
),
),
);
}

View File

@@ -41,7 +41,6 @@ import type {
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2Params,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
@@ -1913,25 +1912,20 @@ export const invalidateGetPublicDashboardDataV2 = async (
*/
export const getPublicDashboardPanelQueryRangeV2 = (
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardPanelQueryRangeV2200>({
url: `/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
method: 'GET',
params,
signal,
});
};
export const getGetPublicDashboardPanelQueryRangeV2QueryKey = (
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
) => {
return [
`/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
...(params ? [params] : []),
] as const;
export const getGetPublicDashboardPanelQueryRangeV2QueryKey = ({
id,
key,
}: GetPublicDashboardPanelQueryRangeV2PathParameters) => {
return [`/api/v2/public/dashboards/${id}/panels/${key}/query_range`] as const;
};
export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
@@ -1939,7 +1933,6 @@ export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
@@ -1952,12 +1945,11 @@ export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key }, params);
getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
> = ({ signal }) =>
getPublicDashboardPanelQueryRangeV2({ id, key }, params, signal);
> = ({ signal }) => getPublicDashboardPanelQueryRangeV2({ id, key }, signal);
return {
queryKey,
@@ -1986,7 +1978,6 @@ export function useGetPublicDashboardPanelQueryRangeV2<
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
@@ -1997,7 +1988,6 @@ export function useGetPublicDashboardPanelQueryRangeV2<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardPanelQueryRangeV2QueryOptions(
{ id, key },
params,
options,
);
@@ -2014,16 +2004,10 @@ export function useGetPublicDashboardPanelQueryRangeV2<
export const invalidateGetPublicDashboardPanelQueryRangeV2 = async (
queryClient: QueryClient,
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
params?: GetPublicDashboardPanelQueryRangeV2Params,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{
queryKey: getGetPublicDashboardPanelQueryRangeV2QueryKey(
{ id, key },
params,
),
},
{ queryKey: getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key }) },
options,
);

View File

@@ -21,7 +21,6 @@ import type {
GetChecks200,
GetChecksParams,
InframonitoringtypesPostableClustersDTO,
InframonitoringtypesPostableContainersDTO,
InframonitoringtypesPostableDaemonSetsDTO,
InframonitoringtypesPostableDeploymentsDTO,
InframonitoringtypesPostableHostsDTO,
@@ -32,7 +31,6 @@ import type {
InframonitoringtypesPostableStatefulSetsDTO,
InframonitoringtypesPostableVolumesDTO,
ListClusters200,
ListContainers200,
ListDaemonSets200,
ListDeployments200,
ListHosts200,
@@ -219,7 +217,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -550,89 +548,6 @@ export const useListJobs = <
> => {
return useMutation(getListJobsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes containers with key kubeletstats metrics: CPU usage (cores), CPU request/limit utilization, memory working set, and memory request/limit utilization. Each container also reports health signals from the k8s_cluster receiver: status (kubectl-style display status derived from k8s.container.status.state + k8s.container.status.reason), restarts (absolute count from k8s.container.restarts), and ready (ready/not_ready from k8s.container.ready). The row identity is (k8s.pod.uid, k8s.container.name), stable across container restarts. Each container includes metadata attributes (k8s.container.name, k8s.pod.name, container.image.name, container.image.tag, k8s.namespace.name, k8s.node.name, k8s.cluster.name, and workload owner such as deployment/statefulset/daemonset/job). The response type is 'list' for the default (k8s.pod.uid, k8s.container.name) grouping (each row is one container with its current status and ready state) or 'grouped_list' for custom groupBy keys (each row aggregates containers in the group with per-status counts under containerCountsByStatus, per-readiness counts under containerCountsByReady, and restarts as the group sum). Status requires the optional k8s.container.status.state and k8s.container.status.reason metrics; when either is missing, status is omitted and a warning is returned while restarts and ready are still computed. Supports filtering via a filter expression, custom groupBy, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, cpuRequestUtilization, cpuLimitUtilization, memory, memoryRequestUtilization, memoryLimitUtilization) and restarts return -1 as a sentinel when no data is available for that field.
* @summary List Kubernetes Containers for Infra Monitoring
*/
export const listContainers = (
inframonitoringtypesPostableContainersDTO?: BodyType<InframonitoringtypesPostableContainersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListContainers200>({
url: `/api/v2/infra_monitoring/kube_containers`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableContainersDTO,
signal,
});
};
export const getListContainersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
> => {
const mutationKey = ['listContainers'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof listContainers>>,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> }
> = (props) => {
const { data } = props ?? {};
return listContainers(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListContainersMutationResult = NonNullable<
Awaited<ReturnType<typeof listContainers>>
>;
export type ListContainersMutationBody =
| BodyType<InframonitoringtypesPostableContainersDTO>
| undefined;
export type ListContainersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Kubernetes Containers for Infra Monitoring
*/
export const useListContainers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
> => {
return useMutation(getListContainersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring

View File

@@ -2813,7 +2813,7 @@ export enum CloudintegrationtypesServiceIDDTO {
cosmosdb = 'cosmosdb',
cassandradb = 'cassandradb',
redis = 'redis',
cloudsql_postgres = 'cloudsql_postgres',
cloudsql = 'cloudsql',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -5005,10 +5005,6 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
* @type string
*/
image?: string;
/**
* @type boolean
*/
legacy: boolean;
/**
* @type boolean
*/
@@ -5084,10 +5080,6 @@ export interface DashboardtypesListedDashboardV2DTO {
* @type string
*/
image?: string;
/**
* @type boolean
*/
legacy: boolean;
/**
* @type boolean
*/
@@ -5585,7 +5577,6 @@ export enum InframonitoringtypesCheckTypeDTO {
namespaces = 'namespaces',
clusters = 'clusters',
volumes = 'volumes',
kube_containers = 'kube_containers',
}
export interface InframonitoringtypesMissingMetricsComponentEntryDTO {
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
@@ -5865,174 +5856,6 @@ export interface InframonitoringtypesClustersDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesContainerCountsByReadyDTO {
/**
* @type integer
*/
notReady: number;
/**
* @type integer
*/
ready: number;
}
export interface InframonitoringtypesContainerCountsByStatusDTO {
/**
* @type integer
*/
completed: number;
/**
* @type integer
*/
containerCannotRun: number;
/**
* @type integer
*/
containerCreating: number;
/**
* @type integer
*/
crashLoopBackOff: number;
/**
* @type integer
*/
createContainerConfigError: number;
/**
* @type integer
*/
errImagePull: number;
/**
* @type integer
*/
error: number;
/**
* @type integer
*/
imagePullBackOff: number;
/**
* @type integer
*/
oomKilled: number;
/**
* @type integer
*/
running: number;
/**
* @type integer
*/
terminated: number;
/**
* @type integer
*/
unknown: number;
/**
* @type integer
*/
waiting: number;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesContainerStatusDTO {
running = 'running',
waiting = 'waiting',
terminated = 'terminated',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
unknown = 'unknown',
no_data = 'no_data',
}
export interface InframonitoringtypesContainerRecordDTO {
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
/**
* @type string
*/
containerName: string;
/**
* @type number
* @format double
*/
cpu: number;
/**
* @type number
* @format double
*/
cpuLimitUtilization: number;
/**
* @type number
* @format double
*/
cpuRequestUtilization: number;
/**
* @type number
* @format double
*/
memory: number;
/**
* @type number
* @format double
*/
memoryLimitUtilization: number;
/**
* @type number
* @format double
*/
memoryRequestUtilization: number;
/**
* @type object,null
*/
meta: InframonitoringtypesContainerRecordDTOMeta;
/**
* @type string
*/
podUID: string;
ready: InframonitoringtypesContainerReadyDTO;
/**
* @type integer
* @format int64
*/
restarts: number;
status: InframonitoringtypesContainerStatusDTO;
}
export interface InframonitoringtypesContainersDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
*/
records: InframonitoringtypesContainerRecordDTO[];
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6090,16 +5913,8 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
/**
* @type integer
*/
misscheduledNodes: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
*/
readyNodes: number;
}
export interface InframonitoringtypesDaemonSetsDTO {
@@ -6623,33 +6438,6 @@ export interface InframonitoringtypesPostableClustersDTO {
start: number;
}
export interface InframonitoringtypesPostableContainersDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesPostableDaemonSetsDTO {
/**
* @type integer
@@ -8496,12 +8284,10 @@ export interface RuletypesBasicRuleThresholdDTO {
op: RuletypesCompareOperatorDTO;
/**
* @type number,null
* @format double
*/
recoveryTarget?: number | null;
/**
* @type number,null
* @format double
*/
target: number | null;
/**
@@ -8695,7 +8481,6 @@ export interface RuletypesRuleConditionDTO {
selectedQueryName?: string;
/**
* @type number,null
* @format double
*/
target?: number | null;
/**
@@ -9194,76 +8979,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
groupId: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesGettableSpanMappersDTO {
/**
* @type array
*/
items: SpantypesSpanMapperDTO[];
}
export enum SpantypesSpanAggregationTypeDTO {
span_count = 'span_count',
execution_time_percentage = 'execution_time_percentage',
@@ -9510,6 +9225,30 @@ export interface SpantypesPostableFlamegraphDTO {
selectedSpanId?: string;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesPostableSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
@@ -9558,6 +9297,45 @@ export interface SpantypesPostableWaterfallDTO {
uncollapsedSpans?: string[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
group_id: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesUpdatableSpanMapperDTO {
config?: SpantypesSpanMapperConfigDTO;
/**
@@ -10859,7 +10637,7 @@ export type ListSpanMappersPathParameters = {
groupId: string;
};
export type ListSpanMappers200 = {
data: SpantypesGettableSpanMappersDTO;
data: SpantypesGettableSpanMapperGroupsDTO;
/**
* @type string
*/
@@ -11263,14 +11041,6 @@ export type ListJobs200 = {
status: string;
};
export type ListContainers200 = {
data: InframonitoringtypesContainersDTO;
/**
* @type string
*/
status: string;
};
export type ListNamespaces200 = {
data: InframonitoringtypesNamespacesDTO;
/**
@@ -11596,19 +11366,6 @@ export type GetPublicDashboardPanelQueryRangeV2PathParameters = {
id: string;
key: string;
};
export type GetPublicDashboardPanelQueryRangeV2Params = {
/**
* @type string
* @description undefined
*/
startTime?: string;
/**
* @type string
* @description undefined
*/
endTime?: string;
};
export type GetPublicDashboardPanelQueryRangeV2200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**

View File

@@ -22,7 +22,6 @@ export const getKeySuggestions = (
fieldContext = '',
fieldDataType = '',
signalSource = '',
metricNamespace = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -31,9 +30,8 @@ export const getKeySuggestions = (
const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
const encodedMetricNamespace = encodeURIComponent(metricNamespace);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}&metricNamespace=${encodedMetricNamespace}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
);
};

View File

@@ -2,7 +2,7 @@ import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
@@ -92,7 +92,6 @@ function CreateServiceAccountModal(): JSX.Element {
width="narrow"
className="create-sa-modal"
disableOutsideClick={isErrorModalVisible}
testId="create-service-account-modal"
>
<div className="create-sa-modal__content">
<form
@@ -135,17 +134,18 @@ function CreateServiceAccountModal(): JSX.Element {
Cancel
</Button>
<AuthZButton
checks={[SACreatePermission]}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</AuthZButton>
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</Button>
</AuthZTooltip>
</DialogFooter>
</DialogWrapper>
);

View File

@@ -1,7 +1,13 @@
import { toast } from '@signozhq/ui/sonner';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
@@ -83,7 +89,7 @@ describe('CreateServiceAccountModal', () => {
await waitFor(() => {
expect(
screen.queryByTestId('create-service-account-modal'),
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
});
@@ -123,7 +129,7 @@ describe('CreateServiceAccountModal', () => {
});
expect(
screen.getByTestId('create-service-account-modal'),
screen.getByRole('dialog', { name: /New Service Account/i }),
).toBeInTheDocument();
});
@@ -131,14 +137,15 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByTestId('create-service-account-modal');
const dialog = await screen.findByRole('dialog', {
name: /New Service Account/i,
});
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(
screen.queryByTestId('create-service-account-modal'),
).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(dialog);
expect(
screen.queryByRole('dialog', { name: /New Service Account/i }),
).not.toBeInTheDocument();
});
it('shows "Name is required" after clearing the name field', async () => {

View File

@@ -3,7 +3,7 @@ import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useExportRawData } from 'hooks/useExportData/useServerExport';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
import { Download, LoaderCircle } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -12,8 +12,6 @@ type BadgeColor =
interface HttpStatusBadgeProps {
statusCode: string | number;
testId?: string;
className?: string;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
@@ -37,8 +35,6 @@ function getStatusCodeColor(statusCode: number): BadgeColor {
function HttpStatusBadge({
statusCode,
testId,
className,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
@@ -49,12 +45,7 @@ function HttpStatusBadge({
const color = getStatusCodeColor(numericStatusCode);
return (
<Badge
color={color}
variant="outline"
data-testid={testId}
className={className}
>
<Badge color={color} variant="outline">
{statusCode}
</Badge>
);

View File

@@ -20,9 +20,9 @@
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
border: 1px solid var(--l2-border);
background: var(--l2-background);
color: var(--l2-foreground);
display: flex;
justify-content: flex-start;
@@ -35,54 +35,21 @@
min-width: 150px;
font-family: 'Space Mono', monospace !important;
--input-border-radius: 0px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-right: none;
border-left: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
font-size: 12px !important;
line-height: 25px;
position: relative;
&:hover,
&:focus {
z-index: 1;
}
.ant-select-selector {
position: relative;
border-radius: inherit;
}
.ant-select:hover .ant-select-selector,
.ant-select-focused .ant-select-selector {
z-index: 1;
}
&.input__has-label-after {
margin-left: -1px;
.ant-select-selector {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.input__has-close-button {
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
~ .close-btn {
margin-left: -1px;
}
}
line-height: 27px;
&::placeholder {
color: var(--input-with-label-color, var(--l3-foreground)) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
&[type='number']::-webkit-inner-spin-button,
@@ -96,35 +63,25 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
position: relative;
&:hover,
&:focus {
z-index: 2;
}
}
&.labelAfter {
.input {
border-radius: 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.label {
border-left: none;
border-radius: 0px 2px 2px 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}

View File

@@ -45,10 +45,7 @@ function InputWithLabel({
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className={cx('input', {
'input__has-label-after': !labelAfter,
'input__has-close-button': !!onClose,
})}
className="input"
placeholder={placeholder}
type={type}
value={inputValue}

View File

@@ -80,8 +80,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -101,8 +101,7 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 6px dotted var(--l1-border);
}
/* Horizontal line pointing from vertical to the item */
@@ -115,8 +114,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -242,8 +241,7 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 6px dotted var(--l1-border);
}
/* Horizontal line pointing from vertical to the item */
@@ -256,8 +254,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -275,16 +273,6 @@
line-height: 16px; /* 128.571% */
resize: none;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
.formula-legend {
@@ -294,42 +282,15 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
);
color: var(--query-builder-v2-color, var(--l2-foreground));
background: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
font-weight: 300;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
height: 36px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
margin-left: -1px;
&:hover,
&:focus {
z-index: 1;
border-color: var(--internal-ant-border-color-hover);
}
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
}
}
@@ -362,8 +323,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -434,8 +395,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -451,7 +412,7 @@
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
@@ -496,16 +457,13 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
height: 36px !important;
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
height: 34px !important;
box-sizing: border-box !important;
.ant-select-selection-item {
color: var(--query-builder-v2-color, var(--l1-foreground));
color: var(--l1-foreground);
}
}

View File

@@ -92,11 +92,6 @@
.ant-select {
width: 100%;
.ant-select-selector {
min-height: 36px;
}
.ant-select-selection-search-input {
min-width: max-content !important;
max-width: 100% !important;
@@ -105,12 +100,9 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
border: 1.005px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -131,7 +123,6 @@
.input {
flex: initial;
width: 100px !important;
min-height: 36px;
}
}
}

View File

@@ -8,9 +8,9 @@
.ant-select-selection-search-input {
font-size: 12px !important;
line-height: 25px;
line-height: 27px;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
}
@@ -22,12 +22,9 @@
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
border: 1px solid var(--l1-border) !important;
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -36,49 +33,36 @@
min-height: 36px;
.ant-select-selection-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
}
}
.qb-select-popover.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
.ant-select-item {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
&:not(:last-of-type) {
margin-bottom: 4px;
}
&:hover,
&.ant-select-item-option-active {
background: var(--l3-background) !important;
}
&:hover,
&.ant-select-item-option-active {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
}
&.ant-select-item-option-selected {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
font-weight: 600;
&.ant-select-item-option-selected {
background: var(--l3-background) !important;
border: 1px solid var(--l1-border);
font-weight: 600;
}
}
}
}

View File

@@ -142,7 +142,6 @@ export const MetricsSelect = memo(function MetricsSelect({
{signalSourceChangeEnabled && (
<Select
className="source-selector"
popupClassName="qb-select-popover"
placeholder="Source"
options={SOURCE_OPTIONS}
value={source}

View File

@@ -1,23 +1,8 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-add-ons {
width: 100%;
--toggle-group-secondary-bg: var(
--query-builder-v2-toggle-group-background-color,
var(--l1-background-hover)
);
--toggle-group-secondary-border: var(
--query-builder-v2-toggle-group-border-color,
var(--l2-border)
);
--toggle-group-secondary-active-bg: var(
--query-builder-v2-toggle-group-active-background-color,
var(--l1-background)
);
--toggle-group-secondary-bg-hover: var(
--query-builder-v2-toggle-group-background-color-hover,
var(--l2-background)
);
.add-on-tab-title {
display: flex;
align-items: center;
@@ -44,33 +29,32 @@
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l2-foreground);
}
> button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 1px solid var(--l1-border);
}
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-border);
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
display: none;
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-border);
}
}
}
@@ -81,7 +65,7 @@
height: 30px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
@@ -94,13 +78,10 @@
align-items: center;
.having-filter-select-container {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: flex-start;
background: var(--query-builder-v2-background-color, var(--l2-background));
padding-right: 38px;
align-items: center;
.having-filter-select-editor {
border-radius: 2px;
@@ -125,17 +106,15 @@
}
.cm-content {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left-width: 0px;
border-right-width: 0px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
@@ -239,32 +218,17 @@
}
.cm-line {
min-height: 34px;
line-height: 32px !important;
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
@@ -273,11 +237,8 @@
}
.chip-decorator {
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -285,38 +246,34 @@
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
.cm-activeLine > span {
font-size: 12px !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
}
.close-btn {
position: absolute;
top: 0;
right: 0;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -343,8 +300,20 @@
font-size: 12px !important;
}
input {
min-height: 36px;
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

@@ -23,7 +23,7 @@
flex: 1;
min-width: 0;
font-size: 12px;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
&.error {
.cm-editor {
@@ -51,15 +51,14 @@
.cm-content {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l1-background) !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
@@ -75,7 +74,7 @@
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
@@ -119,7 +118,7 @@
box-sizing: border-box;
overflow: hidden;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
@@ -128,10 +127,7 @@
&:hover,
&[aria-selected='true'] {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
font-weight: 600 !important;
}
@@ -146,24 +142,15 @@
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
@@ -172,11 +159,8 @@
}
.chip-decorator {
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l1-foreground)) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -184,10 +168,7 @@
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
}
@@ -220,11 +201,12 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l1-border);
background: var(--l1-background);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
@@ -235,13 +217,13 @@
height: 36px;
line-height: 36px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
font-family: 'Space Mono', monospace !important;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l1-foreground);
opacity: 0.5;
}
}
@@ -256,10 +238,9 @@
.query-aggregation-interval-input-container {
.query-aggregation-interval-input {
input {
min-height: 36px;
max-width: 120px;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l2-foreground);
}
}
}
@@ -270,8 +251,8 @@
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background-color: var(--l1-border);
border: 1px solid var(--l1-border);
border-radius: 4px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}

View File

@@ -1,7 +1,7 @@
.add-trace-operator-button,
.add-new-query-button,
.add-formula-button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}

View File

@@ -40,14 +40,11 @@ $max-recents-shown: 5;
.query-status-container {
width: 32px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l1-background) !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
@@ -86,16 +83,16 @@ $max-recents-shown: 5;
.cm-content {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
padding: 0px !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
&.cm-focused {
outline: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
outline: 1px solid var(--l1-border);
}
.cm-tooltip-autocomplete {
@@ -186,17 +183,11 @@ $max-recents-shown: 5;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
background-color: var(--l1-background) !important;
color: var(--l2-foreground) !important;
&:hover {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
}
.cm-completionIcon {
@@ -214,10 +205,7 @@ $max-recents-shown: 5;
}
&[aria-selected='true'] {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
font-weight: 600 !important;
}
}
@@ -286,49 +274,25 @@ $max-recents-shown: 5;
}
.cm-line {
line-height: 36px !important;
line-height: 34px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
.cursor-position {

View File

@@ -89,7 +89,6 @@ interface QuerySearchProps {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
metricNamespace?: string;
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
@@ -108,7 +107,6 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
initialExpression,
metricNamespace,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -301,7 +299,6 @@ function QuerySearch({
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
metricNamespace,
});
if (response.data.data) {
@@ -334,7 +331,6 @@ function QuerySearch({
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
],
);

View File

@@ -65,14 +65,6 @@
display: flex;
flex-direction: column;
gap: 8px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
}
&-aggregation-container {

View File

@@ -47,7 +47,7 @@ function CheckboxValueRow({
{customRendererForValue ? (
customRendererForValue(value)
) : (
<TooltipSimple title={String(value)} side="top" align="start">
<TooltipSimple title={value} side="top" align="center" arrow>
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>

View File

@@ -20,7 +20,6 @@ function OtherFiltersSkeleton(): JSX.Element {
<Skeleton.Input
active
size="small"
className="qf-other-filters-skeleton"
// eslint-disable-next-line react/no-array-index-key
key={index}
/>

View File

@@ -4,7 +4,7 @@ import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
APIKeyCreatePermission,
buildSAAttachPermission,
@@ -109,21 +109,24 @@ function KeyFormPhase({
<Button variant="solid" color="secondary" onClick={onClose}>
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
enabled={!!accountId}
>
Create Key
</AuthZButton>
<Button
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Key
</Button>
</AuthZTooltip>
</div>
</div>
</>

View File

@@ -151,7 +151,6 @@ function AddKeyModal(): JSX.Element {
className="add-key-modal"
showCloseButton
disableOutsideClick={isErrorModalVisible}
testId="add-key-modal"
>
{phase === Phase.FORM && (
<KeyFormPhase

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
@@ -84,18 +84,20 @@ function DeleteAccountModal(): JSX.Element {
<X size={12} />
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[buildSADeletePermission(accountId ?? '')]}
authZEnabled={!!accountId}
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
data-testid="confirm-delete-btn"
enabled={!!accountId}
>
<Trash2 size={12} />
Delete
</AuthZButton>
<Button
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
>
<Trash2 size={12} />
Delete
</Button>
</AuthZTooltip>
</div>
);
@@ -112,7 +114,6 @@ function DeleteAccountModal(): JSX.Element {
className="alert-dialog sa-delete-dialog"
showCloseButton={false}
disableOutsideClick={isErrorModalVisible}
testId="delete-service-account-modal"
footer={footer}
>
{content}

View File

@@ -7,7 +7,6 @@ import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
@@ -159,36 +158,38 @@ function EditKeyForm({
</form>
<div className="edit-key-modal__footer">
<AuthZButton
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(keyItem?.id ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId && !!keyItem?.id}
variant="link"
color="destructive"
onClick={onRevokeClick}
enabled={!!accountId && !!keyItem?.id}
>
<Trash2 size={12} />
Revoke Key
</AuthZButton>
<Button variant="link" color="destructive" onClick={onRevokeClick}>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
<div className="edit-key-modal__footer-right">
<Button variant="solid" color="secondary" onClick={onClose}>
<X size={12} />
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
authZEnabled={!!accountId && !!keyItem?.id}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
enabled={!!accountId && !!keyItem?.id}
>
Save Changes
</AuthZButton>
<Button
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
>
Save Changes
</Button>
</AuthZTooltip>
</div>
</div>
</>

View File

@@ -175,7 +175,6 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
}
showCloseButton={!isRevokeConfirmOpen}
disableOutsideClick={isErrorModalVisible}
testId="edit-key-modal"
footer={
isRevokeConfirmOpen ? (
<RevokeKeyFooter

View File

@@ -1,13 +1,12 @@
import React, { useCallback, useMemo } from 'react';
import { KeyRound, X } from '@signozhq/icons';
import { Pagination, Skeleton, Table, Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Skeleton, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildAPIKeyDeletePermission,
buildSAAttachPermission,
buildSADetachPermission,
@@ -25,10 +24,10 @@ interface KeysTabProps {
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
isLoading: boolean;
isDisabled?: boolean;
canUpdate?: boolean;
accountId?: string;
currentPage: number;
pageSize: number;
onPageChange: (page: number) => void;
}
interface BuildColumnsParams {
@@ -114,26 +113,29 @@ function buildColumns({
render: (_, record): JSX.Element => {
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
return (
<Tooltip title={tooltipTitle}>
<AuthZButton
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
authZEnabled={!isDisabled && !!accountId}
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</AuthZButton>
</Tooltip>
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
enabled={!isDisabled && !!accountId}
>
<Tooltip title={tooltipTitle}>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
</AuthZTooltip>
);
},
},
@@ -147,7 +149,6 @@ function KeysTab({
accountId = '',
currentPage,
pageSize,
onPageChange,
}: KeysTabProps): JSX.Element {
const [, setIsAddKeyOpen] = useQueryState(
'add-key',
@@ -211,18 +212,21 @@ function KeysTab({
Learn more
</a>
</p>
<AuthZButton
<AuthZTooltip
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
authZEnabled={!isDisabled && !!accountId}
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
enabled={!isDisabled && !!accountId}
>
+ Add your first key
</AuthZButton>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
+ Add your first key
</Button>
</AuthZTooltip>
</div>
);
}
@@ -274,24 +278,6 @@ function KeysTab({
})}
/>
<Pagination
current={currentPage}
pageSize={pageSize}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={onPageChange}
className="sa-drawer__keys-pagination"
/>
<EditKeyModal keyItem={editKey} />
<RevokeKeyModal />
@@ -299,7 +285,4 @@ function KeysTab({
);
}
export default withAuthZContent(KeysTab, {
checks: [APIKeyListPermission],
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
});
export default KeysTab;

View File

@@ -6,20 +6,15 @@ import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import {
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { buildSAUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import SaveErrorItem from './SaveErrorItem';
import type { SaveError } from './utils';
import { Skeleton } from 'antd';
interface OverviewTabProps {
account: ServiceAccountRow;
@@ -28,6 +23,7 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
canUpdate?: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
@@ -43,6 +39,7 @@ function OverviewTab({
localRoles,
onRolesChange,
isDisabled,
canUpdate = true,
availableRoles,
rolesLoading,
rolesError,
@@ -89,22 +86,23 @@ function OverviewTab({
<label className="sa-drawer__label" htmlFor="sa-name">
Name
</label>
{isDisabled ? (
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
{isDisabled || !canUpdate ? (
<AuthZTooltip
checks={[buildSAUpdatePermission(account.id)]}
enabled={!isDisabled && !canUpdate}
>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</AuthZTooltip>
) : (
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
placeholder="Enter name"
/>
</AuthZTooltip>
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
placeholder="Enter name"
/>
)}
</div>
@@ -222,9 +220,4 @@ function OverviewTab({
);
}
export default withAuthZContent(OverviewTab, {
checks: (props): ReturnType<typeof buildSAReadPermission>[] => [
buildSAReadPermission(props.account.id),
],
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
});
export default OverviewTab;

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
buildSADetachPermission,
@@ -45,20 +45,23 @@ export function RevokeKeyFooter({
<X size={12} />
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(keyId ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId && !!keyId}
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
enabled={!!accountId && !!keyId}
>
<Trash2 size={12} />
Revoke Key
</AuthZButton>
<Button
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
</>
);
}
@@ -108,7 +111,7 @@ function RevokeKeyModal(): JSX.Element {
}
function handleCancel(): void {
void setRevokeKeyId(null);
setRevokeKeyId(null);
}
return (

View File

@@ -1,17 +1,11 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Skeleton } from 'antd';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getListServiceAccountsQueryKey,
@@ -22,6 +16,7 @@ import {
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
import { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import {
@@ -33,13 +28,15 @@ import {
RoleUpdateFailure,
useServiceAccountRoleManager,
} from 'hooks/serviceAccount/useServiceAccountRoleManager';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildSAAttachPermission,
buildSADeletePermission,
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import {
parseAsBoolean,
parseAsInteger,
@@ -50,6 +47,7 @@ import {
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AddKeyModal from './AddKeyModal';
import DeleteAccountModal from './DeleteAccountModal';
import KeysTab from './KeysTab';
@@ -72,12 +70,14 @@ function toSaveApiError(err: unknown): APIError {
);
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function ServiceAccountDrawer({
onSuccess,
}: ServiceAccountDrawerProps): JSX.Element {
const [selectedAccountId, setSelectedAccountId] = useQueryState(
SA_QUERY_PARAMS.ACCOUNT,
);
const open = !!selectedAccountId;
const [activeTab, setActiveTab] = useQueryState(
SA_QUERY_PARAMS.TAB,
parseAsStringEnum<ServiceAccountDrawerTab>(
@@ -100,14 +100,28 @@ function ServiceAccountDrawer({
SA_QUERY_PARAMS.DELETE_SA,
parseAsBoolean.withDefault(false),
);
const [localName, setLocalName] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
const queryClient = useQueryClient();
const open = !!selectedAccountId;
const { permissions: drawerPermissions, isLoading: isAuthZLoading } = useAuthZ(
selectedAccountId
? [
buildSAReadPermission(selectedAccountId),
buildSAUpdatePermission(selectedAccountId),
buildSADeletePermission(selectedAccountId),
APIKeyListPermission,
]
: [],
{ enabled: !!selectedAccountId },
);
const canRead =
drawerPermissions?.[buildSAReadPermission(selectedAccountId ?? '')]
?.isGranted ?? false;
const {
data: accountData,
@@ -117,7 +131,7 @@ function ServiceAccountDrawer({
refetch: refetchAccount,
} = useGetServiceAccount(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId } },
{ query: { enabled: canRead && !!selectedAccountId } },
);
const account = useMemo(
@@ -131,7 +145,7 @@ function ServiceAccountDrawer({
isLoading: isRolesLoading,
applyDiff,
} = useServiceAccountRoleManager(selectedAccountId ?? '', {
enabled: !!selectedAccountId,
enabled: canRead && !!selectedAccountId,
});
const roleSessionRef = useRef<string | null>(null);
@@ -180,9 +194,16 @@ function ServiceAccountDrawer({
refetch: refetchRoles,
} = useRoles();
const canListKeys =
drawerPermissions?.[APIKeyListPermission]?.isGranted ?? false;
const canUpdate =
drawerPermissions?.[buildSAUpdatePermission(selectedAccountId ?? '')]
?.isGranted ?? true;
const { data: keysData, isLoading: keysLoading } = useListServiceAccountKeys(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId } },
{ query: { enabled: !!selectedAccountId && canListKeys } },
);
const keys = keysData?.data ?? [];
@@ -196,6 +217,7 @@ function ServiceAccountDrawer({
}
}, [keysLoading, keys.length, keysPage, setKeysPage]);
// the retry for this mutation is safe due to the api being idempotent on backend
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
const retryNameUpdate = useCallback(async (): Promise<void> => {
@@ -353,71 +375,23 @@ function ServiceAccountDrawer({
]);
const handleClose = useCallback((): void => {
setSaveErrors([]);
void setIsDeleteOpen(null);
void setIsAddKeyOpen(null);
void setSelectedAccountId(null);
void setActiveTab(null);
void setKeysPage(null);
void setEditKeyId(null);
void setIsAddKeyOpen(null);
void setIsDeleteOpen(null);
void setSelectedAccountId(null);
setSaveErrors([]);
}, [
setSelectedAccountId,
setActiveTab,
setKeysPage,
setEditKeyId,
setIsAddKeyOpen,
setIsDeleteOpen,
setSelectedAccountId,
]);
const footer = useMemo(
() =>
activeTab === ServiceAccountDrawerTab.Overview && !isDeleted && open ? (
<div className="sa-drawer__footer">
<AuthZButton
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</AuthZButton>
<div className="sa-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<AuthZButton
checks={[buildSAUpdatePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</AuthZButton>
</div>
</div>
) : null,
[
activeTab,
isDeleted,
open,
selectedAccountId,
isSaving,
isDirty,
handleClose,
handleSave,
setIsDeleteOpen,
],
);
const body = (
const drawerContent = (
<div className="sa-drawer__layout">
<div className="sa-drawer__tabs">
<ToggleGroupSimple
@@ -459,23 +433,26 @@ function ServiceAccountDrawer({
]}
/>
{activeTab === ServiceAccountDrawerTab.Keys && (
<AuthZButton
<AuthZTooltip
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(selectedAccountId ?? ''),
]}
authZEnabled={!isDeleted && !!selectedAccountId}
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
enabled={!isDeleted && !!selectedAccountId}
>
<Plus size={12} />
Add Key
</AuthZButton>
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Plus size={12} />
Add Key
</Button>
</AuthZTooltip>
)}
</div>
@@ -484,7 +461,9 @@ function ServiceAccountDrawer({
activeTab === ServiceAccountDrawerTab.Keys ? ' sa-drawer__body--keys' : ''
}`}
>
{isAccountLoading && <Skeleton active paragraph={{ rows: 6 }} />}
{(isAuthZLoading || isAccountLoading) && (
<Skeleton active paragraph={{ rows: 6 }} />
)}
{isAccountError && (
<ErrorInPlace
error={toAPIError(
@@ -493,73 +472,141 @@ function ServiceAccountDrawer({
)}
/>
)}
{!isAccountLoading && !isAccountError && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(account ? (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
) : (
<Skeleton active />
))}
{activeTab === ServiceAccountDrawerTab.Keys && (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
accountId={selectedAccountId ?? ''}
currentPage={keysPage}
pageSize={PAGE_SIZE}
onPageChange={(page): void => {
void setKeysPage(page);
}}
/>
)}
</>
)}
{!isAuthZLoading &&
!isAccountLoading &&
!isAccountError &&
selectedAccountId && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(canRead && account ? (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
canUpdate={canUpdate}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
) : (
<PermissionDeniedCallout permissionName="serviceaccount:read" />
))}
{activeTab === ServiceAccountDrawerTab.Keys &&
(canListKeys ? (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
canUpdate={canUpdate}
accountId={selectedAccountId}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
) : (
<PermissionDeniedCallout permissionName="factor-api-key:list" />
))}
</>
)}
</div>
</div>
);
return (
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Service Account Details"
className="sa-drawer"
width="wide"
footer={footer}
>
{open && (
const footer = (
<div className="sa-drawer__footer">
{activeTab === ServiceAccountDrawerTab.Keys ? (
<Pagination
current={keysPage}
pageSize={PAGE_SIZE}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={(page): void => {
void setKeysPage(page);
}}
className="sa-drawer__keys-pagination"
/>
) : (
<>
{body}
<DeleteAccountModal />
<AddKeyModal />
{!isDeleted && (
<AuthZTooltip
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
enabled={!!selectedAccountId}
>
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
</AuthZTooltip>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
</div>
)}
</>
)}
</DrawerWrapper>
</div>
);
return (
<>
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Service Account Details"
className="sa-drawer"
width="wide"
footer={footer}
>
{drawerContent}
</DrawerWrapper>
<DeleteAccountModal />
<AddKeyModal />
</>
);
}

View File

@@ -1,8 +1,13 @@
import { toast } from '@signozhq/ui/sonner';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import {
render,
screen,
userEvent,
waitFor,
waitForElementToBeRemoved,
} from 'tests/test-utils';
import AddKeyModal from '../AddKeyModal';
@@ -54,7 +59,6 @@ describe('AddKeyModal', () => {
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json(createdKeyResponse)),
),
setupAuthzAdmin(),
);
});
@@ -91,7 +95,7 @@ describe('AddKeyModal', () => {
await screen.findByText('snz_abc123xyz456secret');
expect(screen.getByText(/Store the key securely/i)).toBeInTheDocument();
expect(screen.getByTestId('add-key-modal')).toBeInTheDocument();
await screen.findByRole('dialog', { name: /Key Created Successfully/i });
});
it('copy button writes key to clipboard and shows toast.success', async () => {
@@ -127,11 +131,9 @@ describe('AddKeyModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await screen.findByTestId('add-key-modal');
const dialog = await screen.findByRole('dialog', { name: /Add a New Key/i });
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(screen.queryByTestId('add-key-modal')).not.toBeInTheDocument();
});
await waitForElementToBeRemoved(dialog);
});
});

View File

@@ -1,6 +1,5 @@
import { toast } from '@signozhq/ui/sonner';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -62,7 +61,6 @@ describe('EditKeyModal (URL-controlled)', () => {
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});
@@ -73,7 +71,9 @@ describe('EditKeyModal (URL-controlled)', () => {
it('renders nothing when edit-key param is absent', () => {
renderModal(null, { account: 'sa-1' });
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
});
it('renders key data from prop when edit-key param is set', async () => {
@@ -100,7 +100,9 @@ describe('EditKeyModal (URL-controlled)', () => {
});
await waitFor(() => {
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
});
});
@@ -127,7 +129,9 @@ describe('EditKeyModal (URL-controlled)', () => {
expect(latestUrlUpdate.queryString).not.toContain('edit-key=');
await waitFor(() => {
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
});
});
@@ -139,7 +143,9 @@ describe('EditKeyModal (URL-controlled)', () => {
await user.click(screen.getByRole('button', { name: /Revoke Key/i }));
// Same dialog, now showing revoke confirmation
expect(screen.getByTestId('edit-key-modal')).toBeInTheDocument();
await expect(
screen.findByRole('dialog', { name: /Revoke Original Key Name/i }),
).resolves.toBeInTheDocument();
expect(
screen.getByText(/Revoking this key will permanently invalidate it/i),
).toBeInTheDocument();
@@ -162,7 +168,9 @@ describe('EditKeyModal (URL-controlled)', () => {
});
await waitFor(() => {
expect(screen.queryByTestId('edit-key-modal')).not.toBeInTheDocument();
expect(
screen.queryByRole('dialog', { name: /Edit Key Details/i }),
).not.toBeInTheDocument();
});
});
});

View File

@@ -1,6 +1,5 @@
import { toast } from '@signozhq/ui/sonner';
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -36,7 +35,7 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
{
id: 'key-2',
name: 'Staging Key',
expiresAt: 1924948800, // 2030-12-31 12:00 UTC (noon to avoid timezone issues)
expiresAt: 1924905600, // 2030-12-31
lastObservedAt: '2026-03-10T10:00:00Z',
serviceAccountId: 'sa-1',
},
@@ -48,7 +47,6 @@ const defaultProps = {
isDisabled: false,
currentPage: 1,
pageSize: 10,
onPageChange: jest.fn(),
};
function renderKeysTab(
@@ -69,7 +67,6 @@ describe('KeysTab', () => {
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});
@@ -77,12 +74,9 @@ describe('KeysTab', () => {
server.resetHandlers();
});
it('renders loading state', async () => {
it('renders loading state', () => {
renderKeysTab({ isLoading: true });
// Wait for authz to complete, then check for skeleton
await waitFor(() => {
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
it('renders empty state when no keys and clicking add sets add-key param', async () => {
@@ -97,9 +91,9 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
await expect(
screen.findByText(/No keys. Start by creating one./i),
).resolves.toBeInTheDocument();
expect(
screen.getByText(/No keys. Start by creating one./i),
).toBeInTheDocument();
const addBtn = screen.getByRole('button', { name: /\+ Add your first key/i });
await user.click(addBtn);
expect(onUrlUpdate).toHaveBeenCalledWith(
@@ -109,12 +103,10 @@ describe('KeysTab', () => {
);
});
it('renders table with keys', async () => {
it('renders table with keys', () => {
renderKeysTab();
await expect(
screen.findByText('Production Key'),
).resolves.toBeInTheDocument();
expect(screen.getByText('Production Key')).toBeInTheDocument();
expect(screen.getByText('Staging Key')).toBeInTheDocument();
expect(screen.getByText('Never')).toBeInTheDocument();
expect(screen.getByText('Dec 31, 2030')).toBeInTheDocument();
@@ -130,7 +122,7 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
const row = (await screen.findByText('Production Key')).closest('tr');
const row = screen.getByText('Production Key').closest('tr');
if (!row) {
throw new Error('Row not found');
}
@@ -154,8 +146,6 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
@@ -173,8 +163,7 @@ describe('KeysTab', () => {
renderKeysTab();
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
// Seed the keys cache so RevokeKeyModal can read the key name
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
@@ -188,11 +177,9 @@ describe('KeysTab', () => {
});
});
it('disables actions when isDisabled is true', async () => {
it('disables actions when isDisabled is true', () => {
renderKeysTab({ isDisabled: true });
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -31,6 +32,30 @@ const activeAccountResponse = {
updatedAt: '2026-01-02T00:00:00Z',
};
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
function renderDrawer(
searchParams: Record<string, string> = { account: 'sa-1' },
): ReturnType<typeof render> {
@@ -93,7 +118,7 @@ describe('ServiceAccountDrawer — permissions', () => {
renderDrawer();
await waitFor(() => {
expect(screen.getByText(/read:serviceaccount/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:read/)).toBeInTheDocument();
});
});
@@ -115,7 +140,7 @@ describe('ServiceAccountDrawer — permissions', () => {
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
await waitFor(() => {
expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument();
expect(screen.getByText(/factor-api-key:list/)).toBeInTheDocument();
});
});

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -6,6 +7,30 @@ import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
const ROLES_ENDPOINT = '*/api/v1/roles';
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
@@ -222,20 +247,21 @@ describe('ServiceAccountDrawer', () => {
screen.getByRole('button', { name: /Delete Service Account/i }),
);
await screen.findByTestId('delete-service-account-modal');
expect(
screen.getByTestId('delete-service-account-modal'),
).toBeInTheDocument();
const dialog = await screen.findByRole('dialog', {
name: /Delete service account CI Bot/i,
});
expect(dialog).toBeInTheDocument();
await user.click(screen.getByTestId('confirm-delete-btn'));
const confirmBtns = screen.getAllByRole('button', { name: /^Delete$/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(
() => {
expect(deleteSpy).toHaveBeenCalled();
expect(screen.queryByDisplayValue('CI Bot')).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
await waitFor(() => {
expect(deleteSpy).toHaveBeenCalled();
});
await waitFor(() => {
expect(screen.queryByDisplayValue('CI Bot')).not.toBeInTheDocument();
});
});
it('deleted account shows read-only name, no Save button, no Delete button', async () => {

View File

@@ -65,9 +65,7 @@ function TagKeyValueInput({
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
// Plain Enter adds the tag; let Cmd/Ctrl+Enter pass through so a host form
// (e.g. a modal) can submit on it.
if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey) {
if (e.key === 'Enter') {
e.preventDefault();
commit();
}
@@ -95,17 +93,11 @@ function TagKeyValueInput({
};
const handleEditKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
// Plain Enter commits the edit; let Cmd/Ctrl+Enter pass through so a host
// form (e.g. a modal) can submit on it.
if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey) {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
commitEdit();
} else if (e.key === 'Escape') {
// Contain Escape so it cancels the inline edit instead of bubbling up and
// closing the host drawer/modal.
e.preventDefault();
e.stopPropagation();
cancelEdit();
}
};

View File

@@ -2,22 +2,10 @@
position: sticky;
top: 0;
z-index: 2;
padding: var(
--tanstack-cell-header-padding-top-override,
var(--tanstack-cell-padding-top, 0.3rem)
)
var(
--tanstack-cell-header-padding-right-override,
var(--tanstack-cell-padding-right, 0.3rem)
)
var(
--tanstack-cell-header-padding-bottom-override,
var(--tanstack-cell-padding-bottom, 0.3rem)
)
var(
--tanstack-cell-header-padding-left-override,
var(--tanstack-cell-padding-left, 0.3rem)
);
padding: var(--tanstack-cell-padding-top, 0.3rem)
var(--tanstack-cell-padding-right, 0.3rem)
var(--tanstack-cell-padding-bottom, 0.3rem)
var(--tanstack-cell-padding-left, 0.3rem);
transform: translate3d(
var(--tanstack-header-translate-x, 0px),
var(--tanstack-header-translate-y, 0px),
@@ -251,7 +239,6 @@
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
width: var(--tanstack-table-header-label-width);
}
.tanstackSortIndicator {

View File

@@ -57,13 +57,10 @@ function TimelineV3(props: ITimelineV3Props): JSX.Element {
}
const timeAtCursor = offsetTimestamp + cursorXPercent * spread;
// Use the same width-derived interval spread the ticks use, so the badge
// unit always matches the tick unit (narrow rulers pick fewer intervals).
const intervalSpread = spread / getMinimumIntervalsBasedOnWidth(width);
const unit = getIntervalUnit(intervalSpread, offsetTimestamp);
const unit = getIntervalUnit(spread, offsetTimestamp);
const formatted = toFixed(resolveTimeFromInterval(timeAtCursor, unit), 2);
return `${formatted}${unit.name}`;
}, [cursorXPercent, spread, offsetTimestamp, width]);
}, [cursorXPercent, spread, offsetTimestamp]);
if (endTimestamp < startTimestamp) {
console.error(
@@ -97,17 +94,12 @@ function TimelineV3(props: ITimelineV3Props): JSX.Element {
>
<text
x={index === intervals.length - 1 ? -10 : 0}
y={timelineHeight * 2 - 3}
y={timelineHeight * 2}
fill={strokeColor}
>
{interval.label}
</text>
<line
y1={0}
y2={timelineHeight - 3}
stroke={strokeColor}
strokeWidth="1"
/>
<line y1={0} y2={timelineHeight} stroke={strokeColor} strokeWidth="1" />
</g>
))}
</svg>

View File

@@ -1,63 +0,0 @@
import {
getIntervals,
getIntervalUnit,
getMinimumIntervalsBasedOnWidth,
} from '../utils';
// A tick label looks like "200.00ms" / "1.10s" — grab the trailing unit name.
function unitOfLabel(label: string): string {
const match = label.match(/[a-z]+$/i);
return match ? match[0] : '';
}
describe('getMinimumIntervalsBasedOnWidth', () => {
it('returns fewer intervals for narrower rulers', () => {
expect(getMinimumIntervalsBasedOnWidth(500)).toBe(3);
expect(getMinimumIntervalsBasedOnWidth(700)).toBe(4);
expect(getMinimumIntervalsBasedOnWidth(900)).toBe(5);
expect(getMinimumIntervalsBasedOnWidth(1200)).toBe(6);
});
});
describe('getIntervalUnit', () => {
it('selects the unit from the interval spread', () => {
expect(getIntervalUnit(130, 0).name).toBe('ms');
expect(getIntervalUnit(1100, 0).name).toBe('s');
expect(getIntervalUnit(70_000, 0).name).toBe('m');
});
it('accounts for a large offset (deep-zoom labels stay readable)', () => {
// Cursor spread is tiny but the window starts 5,000,000ms into the trace.
expect(getIntervalUnit(100, 5_000_000).name).toBe('hr');
});
// Regression: the interval COUNT changes the chosen unit, so the crosshair
// badge must use the same width-derived count as the ticks. On a 5.5s trace a
// narrow ruler (5 intervals → 1100ms → "s") and a wide one (6 intervals →
// 916ms → "ms") pick different units.
it('can resolve to different units for the same spread at different counts', () => {
const spread = 5500;
expect(getIntervalUnit(spread / 5, 0).name).toBe('s');
expect(getIntervalUnit(spread / 6, 0).name).toBe('ms');
});
});
describe('badge/tick unit consistency', () => {
// The invariant the fix guarantees: when the badge and the ticks are fed the
// same width-derived intervalSpread, every tick label uses the badge's unit.
it.each([
{ spread: 1287, width: 900 },
{ spread: 5500, width: 900 }, // narrow → 5 intervals, the mismatch case
{ spread: 5500, width: 1200 }, // wide → 6 intervals
{ spread: 120_000, width: 700 },
])('spread=$spread width=$width', ({ spread, width }) => {
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
const intervalSpread = spread / minIntervals;
const badgeUnit = getIntervalUnit(intervalSpread, 0).name;
const intervals = getIntervals(intervalSpread, spread, 0);
intervals.forEach((interval) => {
expect(unitOfLabel(interval.label)).toBe(badgeUnit);
});
});
});

View File

@@ -10,18 +10,14 @@ export type { Interval };
/**
* Select the interval unit matching the timeline's logic.
* Exported so crosshair labels use the same unit as the timeline ticks.
*
* Takes the already-computed `intervalSpread` (spread / minIntervals) rather
* than deriving it from a hardcoded interval count — the tick count is
* width-dependent (`getMinimumIntervalsBasedOnWidth`), so callers must pass the
* same `intervalSpread` the ticks use or the badge unit can diverge from the
* ticks (e.g. ms vs s) on narrower rulers like the waterfall.
* Exported so crosshair labels use the same unit as timeline ticks.
*/
export function getIntervalUnit(
intervalSpread: number,
spread: number,
offsetTimestamp: number,
): IIntervalUnit {
const minIntervals = 6;
const intervalSpread = spread / minIntervals;
const valueForUnitSelection = Math.max(offsetTimestamp, intervalSpread);
let unit: IIntervalUnit = INTERVAL_UNITS[0];
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {

View File

@@ -1,12 +1,12 @@
/* Overlay stays below content */
[data-slot='dialog-overlay'] {
z-index: 1000 !important;
z-index: 50;
}
/* Dialog content always above overlay */
[data-slot='dialog-content'] {
position: fixed;
z-index: 1001 !important;
z-index: 60;
background: var(--l1-background);
color: var(--l1-foreground);

View File

@@ -11,7 +11,6 @@ export enum FeatureKeys {
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_DASHBOARD_V2 = 'use_dashboard_v2',
USE_INFRA_MONITORING_V2 = 'use_infra_monitoring_v2',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
}

View File

@@ -31,6 +31,7 @@ export enum LOCALSTORAGE {
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
FUNNEL_STEPS = 'FUNNEL_STEPS',
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',

View File

@@ -7,7 +7,6 @@ export const REACT_QUERY_KEY = {
AUTO_REFRESH_QUERY: 'AUTO_REFRESH_QUERY',
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
GET_PUBLIC_DASHBOARD_RESOLVED: 'GET_PUBLIC_DASHBOARD_RESOLVED',
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
GET_ALL_LICENCES: 'GET_ALL_LICENCES',

View File

@@ -89,10 +89,8 @@ const ROUTES = {
AI_ASSISTANT_BASE: '/ai-assistant',
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
MCP_SERVER: '/settings/mcp-server',
LLM_OBSERVABILITY_ATTRIBUTE_MAPPING: '/llm-observability/attribute-mapping',
LLM_OBSERVABILITY_BASE: '/llm-observability',
LLM_OBSERVABILITY_OVERVIEW: '/llm-observability/overview',
LLM_OBSERVABILITY_CONFIGURATION: '/llm-observability/configuration',
LLM_OBSERVABILITY_MODEL_PRICING: '/llm-observability/settings/model-pricing',
} as const;
export default ROUTES;

View File

@@ -1,4 +1,3 @@
export enum SESSIONSTORAGE {
RETRY_LAZY_REFRESHED = 'retry-lazy-refreshed',
VIEW_PANEL_HANDOFF = 'view-panel-handoff',
}

View File

@@ -43,7 +43,7 @@
&__title {
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
@@ -51,14 +51,14 @@
&__subtitle {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
&__description {
font-size: var(--periscope-font-size-base);
font-size: 14px;
color: var(--l2-foreground);
line-height: 20px;
}
@@ -67,7 +67,7 @@
margin: 0;
margin-top: 8px;
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
@@ -106,7 +106,7 @@
border: 1px dashed var(--l1-border);
background: transparent;
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
@@ -120,15 +120,15 @@
gap: 6px;
}
// Stack the message and the resources card; card matches the content
// width above it and is capped so it doesn't sprawl in a wide panel.
&__row {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 16px;
width: fit-content;
max-width: 600px;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-end;
max-width: 825px;
gap: 25px;
justify-content: center;
margin-left: 21px;
}
&__content {
@@ -142,7 +142,7 @@
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
width: 100%; // match the content width above
width: 332px;
}
&__resources-title {
@@ -155,6 +155,7 @@
text-transform: uppercase;
padding: 16px 16px 12px;
border-bottom: 1px solid var(--l1-border);
height: 46px;
}
&__resources-links {

View File

@@ -41,28 +41,6 @@
.steps-container {
width: 80%;
.alert-query-section-container {
--query-builder-v2-color: var(--l2-foreground);
--query-builder-v2-background-color: var(--l3-background);
--query-builder-v2-border-color: var(--l3-border);
--query-builder-v2-selection-background-color: var(--l2-background);
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
--query-builder-v2-placeholder-color: var(--l3-foreground);
--query-builder-v2-selected-background-color: var(--l3-foreground);
--query-search-background-color: var(--l3-background);
--query-search-background-color-selection: var(--l3-background);
--input-with-label-border-color: var(--l3-border);
--input-with-label-background-color: var(--l3-background);
--input-with-label-color: var(--l2-foreground);
--query-builder-v2-toggle-group-background-color: var(--l3-background);
--query-builder-v2-toggle-group-border-color: var(--l3-border);
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
--input-height: 36px;
--input-hover-background: var(--l3-background);
--input-hover-border-color: var(--internal-ant-border-color-hover);
}
}
.qb-chart-preview-container {

View File

@@ -1,4 +0,0 @@
.wrapper :global(button[data-color]) {
--checkbox-checked-background: var(--series-color);
--checkbox-border-color: var(--series-color);
}

View File

@@ -3,7 +3,6 @@ import { Checkbox } from '@signozhq/ui/checkbox';
import { CSSProperties } from 'react';
import { CheckBoxProps } from '../types';
import styles from './CustomCheckBox.module.scss';
function CustomCheckBox({
data,
@@ -16,11 +15,12 @@ function CustomCheckBox({
const isChecked = graphVisibilityState[index] || false;
const colorStyle = {
'--series-color': color,
'--checkbox-checked-background': color,
'--checkbox-border-color': color,
} as CSSProperties;
return (
<span className={styles.wrapper} style={colorStyle}>
<span style={colorStyle}>
<Checkbox
onChange={(checked): void => checkBoxOnChangeHandler(checked, index)}
value={isChecked}

View File

@@ -3,30 +3,6 @@
overflow-x: auto;
overflow-y: hidden;
--query-builder-v2-color: var(--l2-foreground);
--query-builder-v2-background-color: var(--l3-background);
--query-builder-v2-border-color: var(--l3-border);
--query-builder-v2-selection-background-color: var(--l2-background);
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
--query-builder-v2-placeholder-color: var(--l3-foreground);
--query-builder-v2-selected-background-color: var(--l3-foreground);
--query-search-background-color: var(--l3-background);
--query-search-background-color-selection: var(--l3-background);
--input-with-label-border-color: var(--l3-border);
--input-with-label-background-color: var(--l3-background);
--input-with-label-color: var(--l2-foreground);
--query-builder-v2-toggle-group-background-color: var(--l3-background);
--query-builder-v2-toggle-group-border-color: var(--l3-border);
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
--input-height: 36px;
--input-hover-background: var(--l3-background);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
--input-background: var(--l3-background);
--input-focus-background: var(--l3-background);
--input-border-color: var(--l3-border);
.full-view-header-container {
display: flex;
flex-direction: column;

View File

@@ -66,7 +66,6 @@ function WidgetGraphComponent({
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
hidePagination,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@@ -431,7 +430,6 @@ function WidgetGraphComponent({
customSeries={customSeries}
customOnRowClick={customOnRowClick}
enableDrillDown={enableDrillDown}
hidePagination={hidePagination}
onColumnWidthsChange={onColumnWidthsChange}
/>
</div>

View File

@@ -42,8 +42,6 @@ export interface WidgetGraphComponentProps {
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
enableDrillDown?: boolean;
/** Hide list-panel pagination controls (e.g. public dashboards, where paging isn't supported). */
hidePagination?: boolean;
}
export interface GridCardGraphProps {

View File

@@ -1,20 +1,15 @@
import { useEffect, useMemo } from 'react';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Button, Skeleton } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import logEvent from 'api/common/logEvent';
import { useListDashboardsForUserV2 } from 'api/generated/services/dashboard';
import {
DashboardtypesListOrderDTO,
DashboardtypesListSortDTO,
} from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useIsDashboardV2 } from 'hooks/useIsDashboardV2';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { ArrowRight, ArrowUpRight, Plus } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { Dashboard } from 'types/api/dashboard/getAll';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
@@ -22,13 +17,6 @@ import dialsUrl from '@/assets/Icons/dials.svg';
import { getItemIcon } from '../constants';
// The five most-recent dashboards, normalised across the v1 and v2 list APIs.
interface RecentDashboard {
id: string;
title: string;
tags: string[];
}
export default function Dashboards({
onUpdateChecklistDoneItem,
loadingUserPreferences,
@@ -38,58 +26,33 @@ export default function Dashboards({
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { user } = useAppContext();
const isDashboardV2 = useIsDashboardV2();
// Fetch the recent dashboards from whichever API the `use_dashboard_v2` flag
// selects; the inactive one stays disabled so it never fires.
const [sortedDashboards, setSortedDashboards] = useState<Dashboard[]>([]);
// Fetch Dashboards
const {
data: v1List,
isLoading: v1Loading,
isError: v1Error,
} = useGetAllDashboard({ enabled: !isDashboardV2 });
const {
data: v2List,
isLoading: v2Loading,
isError: v2Error,
} = useListDashboardsForUserV2(
{
sort: DashboardtypesListSortDTO.updated_at,
order: DashboardtypesListOrderDTO.desc,
limit: 5,
offset: 0,
},
{ query: { enabled: isDashboardV2 } },
);
const isDashboardListLoading = isDashboardV2 ? v2Loading : v1Loading;
const isDashboardListError = isDashboardV2 ? v2Error : v1Error;
const sortedDashboards = useMemo<RecentDashboard[]>(() => {
if (isDashboardV2) {
return (v2List?.data?.dashboards ?? []).map((d) => ({
id: d.id,
title: d.spec?.display?.name ?? d.name,
tags: (d.tags ?? []).map((t) => (t.value ? `${t.key}:${t.value}` : t.key)),
}));
}
return [...(v1List?.data ?? [])]
.sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
)
.slice(0, 5)
.map((d) => ({
id: d.id,
title: d.data.title,
tags: d.data.tags ?? [],
}));
}, [isDashboardV2, v1List, v2List]);
data: dashboardsList,
isLoading: isDashboardListLoading,
isError: isDashboardListError,
} = useGetAllDashboard();
useEffect(() => {
if (!dashboardsList) {
return;
}
const sortedDashboards = dashboardsList.data.sort((a, b) => {
const aUpdateAt = new Date(a.updatedAt).getTime();
const bUpdateAt = new Date(b.updatedAt).getTime();
return bUpdateAt - aUpdateAt;
});
if (sortedDashboards.length > 0 && !loadingUserPreferences) {
onUpdateChecklistDoneItem('SETUP_DASHBOARDS');
}
}, [sortedDashboards, onUpdateChecklistDoneItem, loadingUserPreferences]);
setSortedDashboards(sortedDashboards.slice(0, 5));
}, [dashboardsList, onUpdateChecklistDoneItem, loadingUserPreferences]);
const emptyStateCard = (): JSX.Element => (
<div className="empty-state-container">
@@ -150,7 +113,7 @@ export default function Dashboards({
event.stopPropagation();
logEvent('Homepage: Dashboard clicked', {
dashboardId: dashboard.id,
dashboardName: dashboard.title,
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
@@ -180,12 +143,12 @@ export default function Dashboards({
/>
<div className="alert-rule-item-name home-data-item-name">
{dashboard.title}
{dashboard.data.title}
</div>
</div>
<div className="alert-rule-item-description home-data-item-tag">
{dashboard.tags.map((tag) => (
{dashboard.data.tags?.map((tag) => (
<Badge color="sienna" variant="outline" key={tag}>
{tag}
</Badge>

View File

@@ -189,6 +189,8 @@ function Hosts(): JSX.Element {
queryKeyPrefix="hosts"
tabsConfig={{
showEvents: false,
showContainers: true,
showProcesses: true,
}}
/>
</>

View File

@@ -1,252 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
import StatusFilter from 'container/InfraMonitoringHostsV2/StatusFilter';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import {
getHostMetricsQueryPayload,
hostDetailsMetadataConfig,
hostGetEntityName,
hostGetSelectedItemExpression,
hostInitialEventsExpression,
hostInitialLogTracesExpression,
hostWidgetInfo,
} from './constants';
import {
getHostItemKey,
getHostRowKey,
hostColumnsConfig,
} from './table.config';
import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const compositeQuery = useGetCompositeQueryParam();
const { redirectWithQueryBuilderData } = useQueryBuilder();
const isInitialized = useRef(false);
useEffect(() => {
if (isInitialized.current) {
return;
}
isInitialized.current = true;
if (!compositeQuery) {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
redirectWithQueryBuilderData({
...defaultQuery,
builder: {
...defaultQuery.builder,
queryData: defaultQuery.builder.queryData.map((query) => ({
...query,
filter: { expression: '' },
filters: { items: [], op: 'AND' as const },
})),
},
});
}
}, [compositeQuery, redirectWithQueryBuilderData]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listHosts(
{
filter: {
expression: filters.filter.expression,
filterByStatus: filters.filter.filterByStatus
? (filters.filter.filterByStatus as InframonitoringtypesHostStatusDTO)
: undefined,
},
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
offset: filters.offset,
limit: filters.limit ?? 10,
start: filters.start,
end: filters.end,
orderBy: filters.orderBy
? {
key: { name: filters.orderBy.key.name },
direction:
filters.orderBy.direction === 'asc'
? Querybuildertypesv5OrderDirectionDTO.asc
: Querybuildertypesv5OrderDirectionDTO.desc,
}
: undefined,
},
signal,
);
const data = response.data;
return {
type:
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
? ('grouped_list' as const)
: ('list' as const),
records: data.records,
total: data.total,
endTimeBeforeRetention: data.endTimeBeforeRetention,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch hosts';
return {
type: 'list' as const,
records: [] as InframonitoringtypesHostRecordDTO[],
total: 0,
error: errMsg,
};
}
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesHostRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listHosts(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch host';
return {
data: null,
error: errMsg,
};
}
},
[],
);
const getInitialLogTracesExpression = useCallback(
(host: InframonitoringtypesHostRecordDTO) =>
hostInitialLogTracesExpression(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
<Button
className="periscope-btn ghost"
type="text"
size="small"
onClick={handleFilterVisibilityChange}
>
<Filter size={14} />
</Button>
</div>
) : undefined;
return (
<>
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ rotate: '270deg', cursor: 'pointer' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
/>
</div>
)}
<div
className={`${styles.listContainer}${
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList<InframonitoringtypesHostRecordDTO>
controlListPrefix={controlListPrefix}
leftFilters={<StatusFilter />}
entity={InfraMonitoringEntity.HOSTS}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
getRowKey={getHostRowKey}
getItemKey={getHostItemKey}
eventCategory={InfraMonitoringEvents.HostEntity}
/>
</div>
</div>
</div>
<K8sBaseDetails
category={InfraMonitoringEntity.HOSTS}
eventCategory={InfraMonitoringEvents.HostEntity}
getSelectedItemExpression={hostGetSelectedItemExpression}
fetchEntityData={fetchEntityData}
getEntityName={hostGetEntityName}
getInitialLogTracesExpression={getInitialLogTracesExpression}
getInitialEventsExpression={hostInitialEventsExpression}
metadataConfig={hostDetailsMetadataConfig}
entityWidgetInfo={hostWidgetInfo}
getEntityQueryPayload={getHostMetricsQueryPayload}
queryKeyPrefix="hosts"
tabsConfig={{
showEvents: false,
}}
/>
</>
);
}
export default Hosts;

View File

@@ -1,161 +0,0 @@
.infraMonitoringContainer {
display: flex;
height: 100%;
flex-direction: column;
}
.infraContentRow {
display: flex;
flex-direction: row;
height: calc(100vh - 45px);
width: 100%;
position: relative;
overflow-y: auto;
:global(.periscope-btn) {
&.ghost:not(:disabled) {
border: none;
background: transparent;
&:hover {
background: transparent;
color: var(--bg-robin-500) !important;
font-weight: 500;
}
}
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
}
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
padding: 12px 8px;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
padding-block: 0 !important;
:global(.quick-filters .checkbox-filter) {
padding-left: 18px;
}
}
:global(.quick-filters) {
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
}
}
}
.quickFiltersContainerHeader {
padding: 8px;
border-bottom: 1px solid var(--l1-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.listContainer {
flex: 1;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 595px;
> * {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
}
.quickFiltersToggleContainer {
grid-area: quickFilters;
}
.infraMonitoringTags {
width: fit-content;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
}
.tagsActive {
color: var(--bg-forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
.tagsInactive {
color: var(--bg-slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}

View File

@@ -1,56 +0,0 @@
.statusFilterContainer {
display: flex;
align-items: center;
--toggle-group-item-size: 100%;
--toggle-group-radius: 0px 2px 2px 0px;
--toggle-group-item-font-size: var(--periscope-font-size-base);
}
.statusLabel {
min-width: max-content;
font-size: var(--periscope-font-size-base);
font-weight: var(--periscope-font-weight-regular);
line-height: 18px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
border-right: none;
display: flex;
height: 32px;
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.statusToggleGroup {
height: 32px;
}
.statusToggleItem {
flex: unset;
}
.statusDot {
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
display: inline-block;
}
.allDot {
border: 1px solid var(--text-muted);
background-color: var(--bg-slate-100);
}
.activeDot {
background-color: var(--bg-forest-500);
}
.inactiveDot {
background-color: var(--bg-amber-500);
}

View File

@@ -1,152 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
UrlUpdateEvent,
} from 'nuqs/adapters/testing';
import { AppContext } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import { getAppContextMock } from 'tests/test-utils';
import StatusFilter from './StatusFilter';
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
function renderStatusFilter({
searchParams = {},
onUrlUpdate,
}: {
searchParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
}): ReturnType<typeof render> {
return render(
<MemoryRouter>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppContext.Provider value={getAppContextMock('ADMIN')}>
<NuqsTestingAdapter
searchParams={searchParams}
onUrlUpdate={onUrlUpdate}
>
<StatusFilter />
</NuqsTestingAdapter>
</AppContext.Provider>
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</MemoryRouter>,
);
}
describe('StatusFilter', () => {
beforeEach(() => {
queryClient.clear();
});
it('renders all status options', () => {
renderStatusFilter({});
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'All' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Active' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'Inactive' })).toBeInTheDocument();
});
it('selects "All" by default when no URL param', () => {
renderStatusFilter({});
const allButton = screen.getByRole('radio', { name: 'All' });
expect(allButton).toHaveAttribute('aria-checked', 'true');
});
it('reads "active" from URL and shows Active selected', () => {
renderStatusFilter({ searchParams: { statusFilter: 'active' } });
const activeButton = screen.getByRole('radio', { name: 'Active' });
expect(activeButton).toHaveAttribute('aria-checked', 'true');
});
it('reads "inactive" from URL and shows Inactive selected', () => {
renderStatusFilter({ searchParams: { statusFilter: 'inactive' } });
const inactiveButton = screen.getByRole('radio', { name: 'Inactive' });
expect(inactiveButton).toHaveAttribute('aria-checked', 'true');
});
it('updates URL to "active" when Active clicked', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({ onUrlUpdate });
const activeButton = screen.getByRole('radio', { name: 'Active' });
fireEvent.click(activeButton);
await waitFor(() => {
const statusFilterValue = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('statusFilter'))
.filter(Boolean)
.pop();
expect(statusFilterValue).toBe('active');
});
});
it('updates URL to "inactive" when Inactive clicked', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({ onUrlUpdate });
const inactiveButton = screen.getByRole('radio', { name: 'Inactive' });
fireEvent.click(inactiveButton);
await waitFor(() => {
const statusFilterValue = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('statusFilter'))
.filter(Boolean)
.pop();
expect(statusFilterValue).toBe('inactive');
});
});
it('removes statusFilter from URL when All clicked', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({
searchParams: { statusFilter: 'active' },
onUrlUpdate,
});
const allButton = screen.getByRole('radio', { name: 'All' });
fireEvent.click(allButton);
await waitFor(() => {
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
expect(lastCall[0].searchParams.get('statusFilter')).toBeNull();
});
});
it('resets page when filter changes', async () => {
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderStatusFilter({
searchParams: { page: '3' },
onUrlUpdate,
});
const activeButton = screen.getByRole('radio', { name: 'Active' });
fireEvent.click(activeButton);
await waitFor(() => {
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
const pageValue = lastCall[0].searchParams.get('page');
// page=1 is default, so nuqs removes it from URL (null) or keeps as "1"
expect(pageValue === null || pageValue === '1').toBe(true);
});
});
});

View File

@@ -1,63 +0,0 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import {
StatusFilterValue,
useInfraMonitoringPageListing,
useInfraMonitoringStatusFilter,
} from 'container/InfraMonitoringK8sV2/hooks';
import styles from './StatusFilter.module.scss';
const statusOptions: Array<{
label: string;
value: StatusFilterValue | 'all';
}> = [
{ label: 'All', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
];
function StatusFilter(): JSX.Element {
const [statusFilter, setStatusFilter] = useInfraMonitoringStatusFilter();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const handleChange = (value: string): void => {
if (value !== undefined) {
void setStatusFilter(value === 'all' ? '' : (value as StatusFilterValue));
void setCurrentPage(1);
}
};
return (
<div className={styles.statusFilterContainer}>
<div className={styles.statusLabel}>Status</div>
<ToggleGroup
type="single"
value={statusFilter === '' ? 'all' : statusFilter}
onChange={handleChange}
className={styles.statusToggleGroup}
>
{statusOptions.map((option) => (
<ToggleGroupItem
key={option.value}
value={option.value}
aria-label={option.label}
className={styles.statusToggleItem}
>
<span
className={`${styles.statusDot} ${
option.value === 'active'
? styles.activeDot
: option.value === 'inactive'
? styles.inactiveDot
: styles.allDot
}`}
/>
{option.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
);
}
export default StatusFilter;

View File

@@ -1,132 +0,0 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Badge } from '@signozhq/ui/badge';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import {
getHostQueryPayload,
hostWidgetInfo,
} from 'container/LogDetailedView/InfraMetrics/constants';
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
export function getProgressColor(percent: number): string {
if (percent >= 90) {
return Color.BG_SAKURA_500;
}
if (percent >= 60) {
return Color.BG_AMBER_500;
}
return Color.BG_FOREST_500;
}
export function getMemoryProgressColor(percent: number): string {
if (percent >= 90) {
return Color.BG_CHERRY_500;
}
if (percent >= 60) {
return Color.BG_AMBER_500;
}
return Color.BG_FOREST_500;
}
export type HostDetailMetadataConfigType =
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
{
label: 'STATUS',
getValue: (h): string =>
h.status === InframonitoringtypesHostStatusDTO.active
? 'ACTIVE'
: 'INACTIVE',
render: (value, h): React.ReactNode => {
const isActive = h.status === InframonitoringtypesHostStatusDTO.active;
return (
<Badge
variant="outline"
className={`${infraHostsStyles.infraMonitoringTags} ${
isActive ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
}`}
>
{value}
</Badge>
);
},
},
{
label: 'OPERATING SYSTEM',
getValue: (h): string => h.meta?.['os.type'] || '-',
render: (value): React.ReactNode =>
value !== '-' ? (
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
{value}
</Badge>
) : (
<Typography.Text>-</Typography.Text>
),
},
{
label: 'CPU USAGE',
getValue: (h): number => h.cpu * 100,
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
strokeColor={getProgressColor(Number(value))}
showInfo
/>
),
},
{
label: 'MEMORY USAGE',
getValue: (h): number => h.memory * 100,
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
strokeColor={getMemoryProgressColor(Number(value))}
showInfo
/>
),
},
];
export function getHostMetricsQueryPayload(
host: InframonitoringtypesHostRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): ReturnType<typeof getHostQueryPayload> {
return getHostQueryPayload(host.hostName, start, end, dotMetricsEnabled);
}
export { hostWidgetInfo };
export const hostGetSelectedItemExpression = (hostName: string): string =>
`host.name = ${formatValueForExpression(hostName)}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,
dotMetricsEnabled: boolean,
): string {
const hostKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostName = formatValueForExpression(host.hostName || '');
return `${hostKey} = ${hostName}`;
}
export function hostInitialEventsExpression(
_host: InframonitoringtypesHostRecordDTO,
): string {
return '';
}
export const hostGetEntityName = (
host: InframonitoringtypesHostRecordDTO,
): string => host.hostName;

View File

@@ -1,69 +0,0 @@
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataSource } from 'types/common/queryBuilder';
import Hosts from './Hosts';
function InfraMonitoringHosts(): JSX.Element {
const {
updateAllQueriesOperators,
handleSetConfig,
setSupersetQuery,
setLastUsedQuery,
currentQuery,
resetQuery,
} = useQueryBuilder();
useEffect(() => {
const newQuery = updateAllQueriesOperators(
initialQueriesMap.metrics,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
);
setSupersetQuery(newQuery);
setLastUsedQuery(0);
handleSetConfig(PANEL_TYPES.TIME_SERIES, DataSource.METRICS);
return (): void => {
setLastUsedQuery(0);
};
}, [
updateAllQueriesOperators,
setSupersetQuery,
setLastUsedQuery,
handleSetConfig,
]);
useEffect(() => {
const updatedCurrentQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: {
items: [],
op: 'AND',
},
},
],
},
};
resetQuery(updatedCurrentQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Hosts />
</Sentry.ErrorBoundary>
);
}
export default InfraMonitoringHosts;

View File

@@ -1,297 +0,0 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Container } from '@signozhq/icons';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
import {
EntityProgressBar,
ExpandButtonWrapper,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from 'container/InfraMonitoringK8sV2/components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from 'container/InfraMonitoringK8sV2/constants';
import { useInfraMonitoringGroupBy } from 'container/InfraMonitoringK8sV2/hooks';
import ColumnHeader from 'container/InfraMonitoringK8sV2/Base/ColumnHeader';
import EntityGroupHeader from 'container/InfraMonitoringK8sV2/Base/EntityGroupHeader';
import { HostnameCell } from './utils';
import styles from './table.module.scss';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
const statusMap: Record<
InframonitoringtypesHostStatusDTO,
{
label: string;
color: BadgeColor;
}
> = {
[InframonitoringtypesHostStatusDTO.active]: {
label: 'ACTIVE',
color: 'forest',
},
[InframonitoringtypesHostStatusDTO.inactive]: {
label: 'INACTIVE',
color: 'amber',
},
['']: {
label: 'UNKNOWN',
color: 'secondary',
},
};
function hostRowSource(host: InframonitoringtypesHostRecordDTO): {
meta: Record<string, string>;
} {
return {
meta: {
...(host.meta ?? {}),
[INFRA_MONITORING_ATTR_KEYS.HOST_NAME]: host.hostName ?? '',
},
};
}
export function getHostRowKey(host: InframonitoringtypesHostRecordDTO): string {
return host.hostName || 'unknown';
}
export function getHostItemKey(
host: InframonitoringtypesHostRecordDTO,
): string {
return host.hostName ?? '';
}
function HostGroupCell({
row,
}: {
row: InframonitoringtypesHostRecordDTO;
}): JSX.Element {
const [groupBy] = useInfraMonitoringGroupBy();
const synthetic = hostRowSource(row);
return getGroupByEl(synthetic, groupBy) as JSX.Element;
}
export type HostColumnConfigType =
TableColumnDef<InframonitoringtypesHostRecordDTO>;
export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'hostGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Host Group" />,
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-collapse',
cell: ({ row, isExpanded, toggleExpanded }): React.ReactNode => (
<ExpandButtonWrapper isExpanded={isExpanded} toggleExpanded={toggleExpanded}>
<HostGroupCell row={row} />
</ExpandButtonWrapper>
),
},
{
id: 'hostName',
header: (): React.ReactNode => (
<EntityGroupHeader
title="Hostname"
icon={<Container size={14} />}
docPath="/infrastructure-monitoring/host-monitoring#hostname"
/>
),
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<HostnameCell hostName={value as string} />
),
},
{
id: 'status',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Sent system metrics in last 10 mins."
docPath="/infrastructure-monitoring/host-monitoring#status"
>
Status
</ColumnHeader>
),
accessorFn: (row): string => row.status,
width: { min: 140 },
enableSort: false,
cell: ({ value, groupMeta, row }): React.ReactNode => {
const status = value as InframonitoringtypesHostStatusDTO;
if (groupMeta) {
return (
<GroupedStatusCounts
items={[
{
value: row.activeHostCount,
label: 'Active',
color: Color.BG_FOREST_500,
},
{
value: row.inactiveHostCount,
label: 'Inactive',
color: Color.BG_AMBER_500,
},
]}
/>
);
}
const statusDetails = statusMap[status] || statusMap[''];
return (
<Badge
variant="outline"
color={statusDetails.color}
className={`${styles.statusTag}`}
>
{statusDetails.label}
</Badge>
);
},
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
CPU Usage
</ColumnHeader>
),
accessorFn: (row): number => row.cpu,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.HOSTS}
attribute="CPU metric"
>
<EntityProgressBar value={cpu} type="cpu" />
</ValidateColumnValueWrapper>
</div>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
className={styles.memoryUsageHeader}
>
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.memory,
width: { min: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.HOSTS}
attribute="memory metric"
>
<EntityProgressBar value={memory} type="memory" />
</ValidateColumnValueWrapper>
</div>
);
},
},
{
id: 'wait',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#iowait">
IOWait
</ColumnHeader>
),
accessorFn: (row): number => row.wait,
width: { min: 120 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const wait = value as number;
return (
<ValidateColumnValueWrapper
value={wait}
entity={InfraMonitoringEntity.HOSTS}
attribute="IOWait metric"
>
<TanStackTable.Text>
{`${Number((wait * 100).toFixed(1))}%`}
</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'load15',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg (15min)
</ColumnHeader>
),
accessorFn: (row): number => row.load15,
width: { min: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = Number(value);
return (
<ValidateColumnValueWrapper
value={load15}
entity={InfraMonitoringEntity.HOSTS}
attribute="load average metric"
>
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'diskUsage',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
Disk Usage
</ColumnHeader>
),
accessorFn: (row): number => row.diskUsage,
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const diskUsage = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={diskUsage}
entity={InfraMonitoringEntity.HOSTS}
attribute="disk usage metric"
>
<EntityProgressBar value={diskUsage} type="disk" />
</ValidateColumnValueWrapper>
</div>
);
},
},
];

View File

@@ -1,61 +0,0 @@
.entityGroupHeader {
padding-left: var(--spacing-5);
gap: var(--spacing-5);
display: flex;
align-items: center;
}
.hostnameColumnHeader {
display: block;
}
.columnHeaderRight {
text-align: right;
}
.memoryUsageHeader {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-2);
width: 100%;
}
.statusTag {
width: fit-content;
font-family: Inter, sans-serif;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
}
.statusTagActive {
color: var(--Forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
.statusTagInactive {
color: var(--Slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}
.progressContainer {
display: flex;
align-items: center;
& > div {
width: 100%;
}
}
.progressBar {
flex: 1;
margin-right: 8px;
}

View File

@@ -1,124 +0,0 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
FiltersType,
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { TriangleAlert } from '@signozhq/icons';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import TanStackTable from 'components/TanStackTableView';
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
export function HostnameCell({
hostName,
}: {
hostName?: string | null;
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
}
return (
<>
<Typography.Text color="muted">-</Typography.Text>
<Tooltip
title={
<div>
Missing host.name metadata.
<br />
<a
href={HOSTNAME_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn how to configure
</a>
</div>
}
trigger={['hover', 'focus']}
>
<span
className="hostname-cell-warning-icon"
tabIndex={0}
role="img"
aria-label="Missing host.name metadata"
onClick={(e): void => e.stopPropagation()}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
}
}}
>
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
</span>
</Tooltip>
</>
);
}
export function getHostsQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {
const hostNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
const metricName = dotMetricsEnabled
? 'system.cpu.load_average.15m'
: 'system_cpu_load_average_15m';
const environmentKey = dotMetricsEnabled
? 'deployment.environment'
: 'deployment_environment';
return [
{
type: FiltersType.CHECKBOX,
title: 'Host Name',
attributeKey: {
key: hostNameKey,
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: metricName,
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'OS Type',
attributeKey: {
key: osTypeKey,
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: metricName,
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: environmentKey,
dataType: DataTypes.String,
type: 'resource',
},
defaultOpen: true,
},
];
}

View File

@@ -35,6 +35,7 @@ import {
ChevronsLeftRight,
Compass,
DraftingCompass,
Package2,
ScrollText,
X,
} from '@signozhq/icons';
@@ -53,10 +54,12 @@ import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityContainers from '../EntityDetailsUtils/EntityContainers';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityProcesses from '../EntityDetailsUtils/EntityProcesses';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
@@ -117,6 +120,8 @@ export interface K8sBaseDetailsProps<T> {
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
showContainers?: boolean;
showProcesses?: boolean;
};
customTabs?: Array<{
key: string;
@@ -254,6 +259,8 @@ export default function K8sBaseDetails<T>({
showLogs: true,
showTraces: true,
showEvents: true,
showContainers: false,
showProcesses: false,
...tabsConfig,
}),
[tabsConfig],
@@ -568,6 +575,32 @@ export default function K8sBaseDetails<T>({
},
]
: []),
...(tabVisibility.showContainers
? [
{
value: VIEW_TYPES.CONTAINERS,
label: (
<div className="view-title">
<Package2 size={14} />
Containers
</div>
),
},
]
: []),
...(tabVisibility.showProcesses
? [
{
value: VIEW_TYPES.PROCESSES,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Processes
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
@@ -647,6 +680,11 @@ export default function K8sBaseDetails<T>({
initialExpression={eventsInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.CONTAINERS &&
tabVisibility.showContainers && <EntityContainers />}
{effectiveView === VIEW_TYPES.PROCESSES && tabVisibility.showProcesses && (
<EntityProcesses />
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<React.Fragment key={tab.key}>

View File

@@ -0,0 +1,47 @@
import { useTranslation } from 'react-i18next';
import { Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
import broomUrl from '@/assets/Icons/broom.svg';
import infraContainersUrl from '@/assets/Icons/infraContainers.svg';
import 'components/HostMetricsDetail/Containers/Containers.styles.scss';
const { Text } = Typography;
function EntityContainers(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-containers" size={24}>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src={infraContainersUrl}
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('containers_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src={broomUrl} alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="containers" />
</div>
</Space>
);
}
export default EntityContainers;

View File

@@ -0,0 +1,46 @@
import { useTranslation } from 'react-i18next';
import { Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
import broomUrl from '@/assets/Icons/broom.svg';
import infraContainersUrl from '@/assets/Icons/infraContainers.svg';
import 'components/HostMetricsDetail/Processes/Processes.styles.scss';
const { Text } = Typography;
function EntityProcesses(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-processes" size={24}>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src={infraContainersUrl}
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('processes_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src={broomUrl} alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="processes" />
</div>
</Space>
);
}
export default EntityProcesses;

View File

@@ -1,98 +0,0 @@
import { act, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import { renderEntityTraces, verifyQueryPayload } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
describe('EntityTraces - Default Behavior', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithTracesResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
});
it('should fetch traces using V5 API on initial render', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedOffset: 0,
});
});
it('should pass time range to API (converted to milliseconds)', async () => {
const timeRange = { startTime: 1000, endTime: 2000 };
act(() => {
renderEntityTraces({ timeRange });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedTimeRange: {
start: timeRange.startTime * 1000,
end: timeRange.endTime * 1000,
},
});
});
it('should order results by timestamp desc', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
order?: Array<{ key: { name: string }; direction: string }>;
};
const timestampOrder = spec.order?.find((o) => o.key.name === 'timestamp');
expect(timestampOrder?.direction).toBe('desc');
});
it('should use TRACES signal type', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const query = capturedPayloads[0].compositeQuery.queries[0];
const spec = query?.spec as { signal?: string };
expect(spec?.signal).toBe('traces');
});
it('should use raw request type for traces list view', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
expect(capturedPayloads[0].requestType).toBe('raw');
});
});

View File

@@ -1,38 +0,0 @@
import { act, screen } from 'tests/test-utils';
import { renderEntityTraces } from './testUtils';
import { mockQueryRangeV5WithEmptyTraces } from '__tests__/query_range_v5.util';
describe('EntityTraces - Empty State', () => {
it('should show empty state when no traces returned', async () => {
mockQueryRangeV5WithEmptyTraces();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
});
it('should not show table when traces are empty', async () => {
mockQueryRangeV5WithEmptyTraces();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
expect(screen.queryByRole('table')).not.toBeInTheDocument();
});
it('should not show pagination controls when traces are empty', async () => {
mockQueryRangeV5WithEmptyTraces();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
expect(screen.queryByText(/1 - 10/)).not.toBeInTheDocument();
});
});

View File

@@ -1,44 +0,0 @@
import { act, screen } from 'tests/test-utils';
import { renderEntityTraces } from './testUtils';
import {
mockQueryRangeV5WithError,
mockQueryRangeV5WithKeyNotFoundError,
} from '__tests__/query_range_v5.util';
describe('EntityTraces - Error State', () => {
it('should show error state on API error', async () => {
mockQueryRangeV5WithError('Internal server error', 500);
act(() => {
renderEntityTraces();
});
await expect(
screen.findByText(/Something went wrong/i),
).resolves.toBeInTheDocument();
});
it('should show empty state for key not found error', async () => {
mockQueryRangeV5WithKeyNotFoundError();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
});
it('should not show table on API error', async () => {
mockQueryRangeV5WithError('Internal server error', 500);
act(() => {
renderEntityTraces();
});
await expect(
screen.findByText(/Something went wrong/i),
).resolves.toBeInTheDocument();
expect(screen.queryByRole('table')).not.toBeInTheDocument();
});
});

View File

@@ -1,55 +0,0 @@
import { act, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import { renderEntityTraces } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
describe('EntityTraces - Expression', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithTracesResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
});
it('should include expression in query filter', async () => {
const expression = 'k8s.pod.name = "my-pod"';
act(() => {
renderEntityTraces({ expression });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
filter?: { expression: string };
};
expect(spec.filter?.expression).toContain('k8s.pod.name');
});
it('should include complex expression with multiple conditions', async () => {
const expression = 'k8s.pod.name = "my-pod" AND service.name = "api"';
act(() => {
renderEntityTraces({ expression });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
filter?: { expression: string };
};
expect(spec.filter?.expression).toContain('k8s.pod.name');
expect(spec.filter?.expression).toContain('service.name');
});
});

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