Compare commits

..

1 Commits

Author SHA1 Message Date
Tushar Vats
ece21d909a fix: added support for serach in logs 2026-07-13 06:07:29 +05:30
459 changed files with 9964 additions and 21853 deletions

2
.github/CODEOWNERS vendored
View File

@@ -189,9 +189,7 @@ go.mod @therealpandey
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHostsV2/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8sV2/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend

View File

@@ -543,31 +543,6 @@ components:
required:
- id
type: object
AuthtypesGettableRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesGettableToken:
properties:
accessToken:
@@ -719,6 +694,43 @@ components:
- detach
type: string
AuthtypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
type: string
groupMappings:
additionalProperties:
type: string
nullable: true
type: object
useRoleAttribute:
type: boolean
type: object
AuthtypesRoleWithTransactionGroups:
properties:
createdAt:
format: date-time
@@ -746,18 +758,6 @@ components:
- orgId
- transactionGroups
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
type: string
groupMappings:
additionalProperties:
type: string
nullable: true
type: object
useRoleAttribute:
type: boolean
type: object
AuthtypesSamlConfig:
properties:
attributeMapping:
@@ -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:
@@ -4259,34 +4253,6 @@ components:
type: number
clusterName:
type: string
counts:
properties:
daemonSets:
format: int64
type: integer
deployments:
format: int64
type: integer
jobs:
format: int64
type: integer
namespaces:
format: int64
type: integer
nodes:
format: int64
type: integer
statefulSets:
format: int64
type: integer
required:
- nodes
- namespaces
- deployments
- daemonSets
- jobs
- statefulSets
type: object
meta:
additionalProperties:
type: string
@@ -4307,7 +4273,6 @@ components:
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesClusters:
@@ -4513,14 +4478,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
@@ -4531,8 +4492,6 @@ components:
- daemonSetMemoryLimit
- desiredNodes
- currentNodes
- readyNodes
- misscheduledNodes
- podCountsByPhase
- podCountsByStatus
- meta
@@ -4829,26 +4788,6 @@ components:
type: object
InframonitoringtypesNamespaceRecord:
properties:
counts:
properties:
daemonSets:
format: int64
type: integer
deployments:
format: int64
type: integer
jobs:
format: int64
type: integer
statefulSets:
format: int64
type: integer
required:
- deployments
- daemonSets
- jobs
- statefulSets
type: object
meta:
additionalProperties:
type: string
@@ -4872,7 +4811,6 @@ components:
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesNamespaces:
@@ -7456,11 +7394,9 @@ components:
op:
$ref: '#/components/schemas/RuletypesCompareOperator'
recoveryTarget:
format: double
nullable: true
type: number
target:
format: double
nullable: true
type: number
targetUnit:
@@ -7744,7 +7680,6 @@ components:
selectedQueryName:
type: string
target:
format: double
nullable: true
type: number
targetUnit:
@@ -12072,7 +12007,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesGettableRole'
$ref: '#/components/schemas/AuthtypesRole'
type: array
status:
type: string
@@ -12256,7 +12191,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesRole'
$ref: '#/components/schemas/AuthtypesRoleWithTransactionGroups'
status:
type: string
required:
@@ -16055,24 +15990,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:

View File

@@ -119,6 +119,10 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return results, nil
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return provider.openfgaServer.Write(ctx, additions, deletions)
}
@@ -127,6 +131,10 @@ func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.Re
return provider.openfgaServer.ReadTuples(ctx, tupleKey)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
@@ -175,7 +183,7 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -200,31 +208,70 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
return err
}
return provider.store.Create(ctx, role)
if err := provider.store.Create(ctx, role.Role); err != nil {
return err
}
return nil
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.store.Get(ctx, orgID, id)
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
}
if existingRole != nil {
return existingRole, nil
}
err = provider.store.Create(ctx, role)
if err != nil {
return nil, err
}
return role, nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.Role) error {
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return nil, err
}
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.Get(ctx, orgID, updatedRole.ID)
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
if err != nil {
return err
}
existingTuples, err := provider.readAllTuplesForRole(ctx, existingRole.Name, orgID)
if err != nil {
return err
}
existingGroups := authtypes.MustNewTransactionGroupsFromTuples(existingTuples)
additions, deletions := existingGroups.Diff(updatedRole.TransactionGroups)
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
@@ -240,7 +287,7 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
return err
}
return provider.store.Update(ctx, orgID, updatedRole)
return provider.store.Update(ctx, orgID, updatedRole.Role)
}
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
@@ -249,7 +296,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.Get(ctx, orgID, id)
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
if err != nil {
return err
}
@@ -265,7 +312,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
if err != nil {
return err
}
@@ -277,24 +324,6 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.openfgaServer.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
tuples := []*openfgav1.TupleKey{}
@@ -346,3 +375,21 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
return tuples
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}

View File

@@ -105,6 +105,10 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
}
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return server.pkgAuthzService.Write(ctx, additions, deletions)
}

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

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

@@ -0,0 +1,29 @@
import { PropsWithChildren } from 'react';
type CommonProps = PropsWithChildren<{
className?: string;
minSize?: number;
maxSize?: number;
defaultSize?: number;
direction?: 'horizontal' | 'vertical';
autoSaveId?: string;
withHandle?: boolean;
}>;
export function ResizablePanelGroup({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizablePanel({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizableHandle({ className }: CommonProps): JSX.Element {
return <div className={className} />;
}

View File

@@ -20,7 +20,7 @@ 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,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,

View File

@@ -49,6 +49,7 @@
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.23",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
@@ -74,7 +75,7 @@
"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",
@@ -127,7 +128,7 @@
"timestamp-nano": "^1.0.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "14.0.1",
"uuid": "^8.3.2",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"zod": "4.3.6",
@@ -221,5 +222,26 @@
"*.(scss|css)": [
"stylelint"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "4.1.1",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"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"
}
}

1960
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,6 @@
blockExoticSubdeps: true
trustPolicy: no-downgrade
minimumReleaseAge: 2880 # 2d
minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- '@signozhq/*'
minimumReleaseAgeStrict: true
# Security floors for vulnerable transitive deps. Where possible, targets are
# capped to avoid crossing breaking versions (major; and minor for 0.x).
# Some entries may still force a breaking bump when no safe release exists
# within the consumers constraint—see per-entry notes.
overrides:
# via: direct devDep @babel/core ^7.22.11 (+ babel plugin peers)
# remove: bump @babel/core in package.json to ^7.29.6
'@babel/core@<=7.29.0': '>=7.29.6 <8'
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
cookie@<0.7.0: '>=0.7.1 <1'
# via: direct dep dompurify 3.4.0; @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
# @monaco-editor/react > monaco-editor@0.55.1 (3.2.7 exact)
# remove: bump direct dep to 3.4.11; @grafana/data (latest 13.1.0) and
# monaco-editor (latest 0.55.1) still pin vulnerable versions — blocked
dompurify@<=3.4.10: '>=3.4.11 <4'
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); orval@8.9.1 (^0.27.4); ts-jest@29.4.9 (~0.27.4)
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
js-cookie@<=3.0.5: '>=3.0.7 <4'
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
prismjs@<1.30.0: '>=1.30.0 <2'
# via: direct dep react-router-dom-v5-compat@6.30.3 (react-router 6.30.3 exact)
# remove: bump react-router-dom-v5-compat to 6.30.4. Do NOT open the cap:
# react-router >=7 requires React 19 and breaks the app-wide CompatRouter
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
# via: msw@1.3.2 (devDep) > inquirer@8 > external-editor@3.1.0 (tmp ^0.0.33)
# remove: upgrade msw to >=2 (drops the inquirer/external-editor chain)
tmp@<0.2.6: '>=0.2.6 <0.3.0'
# via: jest > babel-plugin-macros > cosmiconfig@7 (yaml ^1.10.0);
# typescript-plugin-css-modules > postcss-load-config@3 (^1.10.2)
# remove: blocked — babel-plugin-macros 3.1.0 (latest) still uses cosmiconfig@7
yaml@>=1.0.0 <1.10.3: '>=1.10.3 <2'
trustPolicy: no-downgrade
trustPolicyExclude:
- 'semver@6.3.1 || 5.7.2'
blockExoticSubdeps: true

View File

@@ -219,7 +219,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 = (

View File

@@ -2082,39 +2082,6 @@ export interface AuthtypesGettableAuthDomainDTO {
updatedAt?: string;
}
export interface AuthtypesGettableRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface AuthtypesGettableTokenDTO {
/**
* @type string
@@ -2358,6 +2325,39 @@ export interface AuthtypesPostableUserRoleDTO {
}
export interface AuthtypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface AuthtypesRoleWithTransactionGroupsDTO {
/**
* @type string
* @format date-time
@@ -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
*/
@@ -5669,39 +5661,6 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
* @format int64
*/
daemonSets: number;
/**
* @type integer
* @format int64
*/
deployments: number;
/**
* @type integer
* @format int64
*/
jobs: number;
/**
* @type integer
* @format int64
*/
namespaces: number;
/**
* @type integer
* @format int64
*/
nodes: number;
/**
* @type integer
* @format int64
*/
statefulSets: number;
};
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -5846,10 +5805,6 @@ export interface InframonitoringtypesClusterRecordDTO {
* @type string
*/
clusterName: string;
/**
* @type object
*/
counts: InframonitoringtypesClusterRecordDTOCounts;
/**
* @type object,null
*/
@@ -6127,16 +6082,8 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
/**
* @type integer
*/
misscheduledNodes: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
*/
readyNodes: number;
}
export interface InframonitoringtypesDaemonSetsDTO {
@@ -6405,29 +6352,6 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
* @format int64
*/
daemonSets: number;
/**
* @type integer
* @format int64
*/
deployments: number;
/**
* @type integer
* @format int64
*/
jobs: number;
/**
* @type integer
* @format int64
*/
statefulSets: number;
};
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6439,10 +6363,6 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesNamespaceRecordDTO {
/**
* @type object
*/
counts: InframonitoringtypesNamespaceRecordDTOCounts;
/**
* @type object,null
*/
@@ -8560,12 +8480,10 @@ export interface RuletypesBasicRuleThresholdDTO {
op: RuletypesCompareOperatorDTO;
/**
* @type number,null
* @format double
*/
recoveryTarget?: number | null;
/**
* @type number,null
* @format double
*/
target: number | null;
/**
@@ -8759,7 +8677,6 @@ export interface RuletypesRuleConditionDTO {
selectedQueryName?: string;
/**
* @type number,null
* @format double
*/
target?: number | null;
/**
@@ -10678,7 +10595,7 @@ export type ListRoles200 = {
/**
* @type array
*/
data: AuthtypesGettableRoleDTO[];
data: AuthtypesRoleDTO[];
/**
* @type string
*/
@@ -10700,7 +10617,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: AuthtypesRoleDTO;
data: AuthtypesRoleWithTransactionGroupsDTO;
/**
* @type string
*/

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

@@ -12,4 +12,5 @@
import '@signozhq/design-tokens';
import '@signozhq/icons';
import '@signozhq/resizable';
import '@signozhq/ui';

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

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

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

@@ -3,7 +3,7 @@ import { Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import APIError from 'types/api/error';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -16,7 +16,7 @@ export interface RoleOption {
}
export function useRoles(): {
roles: AuthtypesGettableRoleDTO[];
roles: AuthtypesRoleDTO[];
isLoading: boolean;
isError: boolean;
error: APIError | undefined;
@@ -33,7 +33,7 @@ export function useRoles(): {
}
export function getRoleOptions(
roles: AuthtypesGettableRoleDTO[],
roles: AuthtypesRoleDTO[],
valueField: 'id' | 'name',
): RoleOption[] {
return roles.map((role) => ({
@@ -79,7 +79,7 @@ interface BaseProps {
placeholder?: string;
className?: string;
getPopupContainer?: (trigger: HTMLElement) => HTMLElement;
roles?: AuthtypesGettableRoleDTO[];
roles?: AuthtypesRoleDTO[];
loading?: boolean;
isError?: boolean;
error?: APIError;

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

@@ -4,22 +4,17 @@ import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
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,7 +23,8 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
availableRoles: AuthtypesGettableRoleDTO[];
canUpdate?: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
rolesErrorObj?: APIError | undefined;
@@ -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

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

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

@@ -1,37 +1,33 @@
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 logEvent from 'api/common/logEvent';
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 K8sBaseDetails 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 {
useInfraMonitoringFiltersK8s,
useInfraMonitoringPageListing,
} from 'container/InfraMonitoringK8sV2/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
fetchHostEntityData,
fetchHostListData,
getHostMetricsQueryPayload,
hostDetailsMetadataConfig,
hostGetEntityName,
hostGetSelectedItemExpression,
hostInitialEventsExpression,
hostInitialLogTracesExpression,
hostGetSelectedItemFilters,
hostInitialEventsFilter,
hostInitialLogTracesFilter,
hostWidgetInfo,
} from './constants';
import {
@@ -46,135 +42,85 @@ 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 [, setCurrentPage] = useInfraMonitoringPageListing();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
// Track previous urlFilters to only sync when the value actually changes
// (not when handleChangeQueryData changes due to query updates)
const prevUrlFiltersRef = useRef<string | null>(null);
useEffect(() => {
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
// Only sync if urlFilters value has actually changed
if (prevUrlFiltersRef.current !== currentFiltersJson) {
prevUrlFiltersRef.current = currentFiltersJson;
// Sync filters to query builder, using empty filter when urlFilters is null
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
const handleQuickFiltersChange = (query: Query): void => {
const filters = query.builder.queryData[0].filters;
// Nuqs batches these calls into a single URL update
// The useEffect will sync filters to query builder
setUrlFilters(filters || null);
setCurrentPage(1);
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.HostEntity,
page: InfraMonitoringEvents.ListPage,
});
};
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
return fetchHostListData(filters, signal);
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
filters: Parameters<typeof fetchHostEntityData>[0],
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,
};
}
},
) => fetchHostEntityData(filters, signal),
[],
);
const getInitialLogTracesExpression = useCallback(
(host: InframonitoringtypesHostRecordDTO) =>
hostInitialLogTracesExpression(host, dotMetricsEnabled),
const getSelectedItemFilters = useCallback(
(selectedItem: string) =>
hostGetSelectedItemFilters(selectedItem, dotMetricsEnabled),
[dotMetricsEnabled],
);
const getInitialLogTracesFilters = useCallback(
(host: import('api/infraMonitoring/getHostLists').HostData) =>
hostInitialLogTracesFilter(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
<Button
@@ -208,6 +154,7 @@ function Hosts(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</div>
)}
@@ -216,9 +163,8 @@ function Hosts(): JSX.Element {
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList<InframonitoringtypesHostRecordDTO>
<K8sBaseList
controlListPrefix={controlListPrefix}
leftFilters={<StatusFilter />}
entity={InfraMonitoringEntity.HOSTS}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
@@ -232,11 +178,11 @@ function Hosts(): JSX.Element {
<K8sBaseDetails
category={InfraMonitoringEntity.HOSTS}
eventCategory={InfraMonitoringEvents.HostEntity}
getSelectedItemExpression={hostGetSelectedItemExpression}
getSelectedItemFilters={getSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={hostGetEntityName}
getInitialLogTracesExpression={getInitialLogTracesExpression}
getInitialEventsExpression={hostInitialEventsExpression}
getInitialLogTracesFilters={getInitialLogTracesFilters}
getInitialEventsFilters={hostInitialEventsFilter}
metadataConfig={hostDetailsMetadataConfig}
entityWidgetInfo={hostWidgetInfo}
getEntityQueryPayload={getHostMetricsQueryPayload}

View File

@@ -26,8 +26,8 @@
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
@@ -35,12 +35,11 @@
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
@@ -99,7 +98,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
@@ -114,17 +113,32 @@
.listContainer {
flex: 1;
min-width: 0;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 595px;
> * {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
> :global(.ant-table-wrapper) {
width: 100%;
max-width: 100%;
:global(.ant-spin-container) {
display: flex !important;
flex-direction: column;
}
:global(.ant-table),
:global(.ant-table-container) {
max-width: 100%;
}
}
}
.listContainerFiltersVisible {
@@ -132,7 +146,7 @@
}
.quickFiltersToggleContainer {
grid-area: quickFilters;
padding: 0 8px;
}
.infraMonitoringTags {

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

@@ -4,16 +4,26 @@ 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';
getHostLists,
HostData,
HostListPayload,
} from 'api/infraMonitoring/getHostLists';
import {
createFilterItem,
K8sDetailsFilters,
K8sDetailsMetadataConfig,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
import {
getHostQueryPayload,
hostWidgetInfo,
} from 'container/LogDetailedView/InfraMetrics/constants';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { getHostListsQuery } from './utils';
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
@@ -37,32 +47,24 @@ export function getMemoryProgressColor(percent: number): string {
return Color.BG_FOREST_500;
}
export type HostDetailMetadataConfigType =
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
{
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>
);
},
getValue: (h): string => (h.active ? 'ACTIVE' : 'INACTIVE'),
render: (value, h): React.ReactNode => (
<Badge
variant="outline"
className={`${infraHostsStyles.infraMonitoringTags} ${
h.active ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
}`}
>
{value}
</Badge>
),
},
{
label: 'OPERATING SYSTEM',
getValue: (h): string => h.meta?.['os.type'] || '-',
getValue: (h): string => h.os || '-',
render: (value): React.ReactNode =>
value !== '-' ? (
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
@@ -97,7 +99,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
];
export function getHostMetricsQueryPayload(
host: InframonitoringtypesHostRecordDTO,
host: HostData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -107,26 +109,83 @@ export function getHostMetricsQueryPayload(
export { hostWidgetInfo };
export const hostGetSelectedItemExpression = (hostName: string): string =>
`host.name = ${formatValueForExpression(hostName)}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,
export function hostGetSelectedItemFilters(
selectedItem: string,
dotMetricsEnabled: boolean,
): string {
const hostKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostName = formatValueForExpression(host.hostName || '');
return `${hostKey} = ${hostName}`;
): TagFilter {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return {
op: 'AND',
items: [createFilterItem(hostKey, selectedItem)],
};
}
export function hostInitialEventsExpression(
_host: InframonitoringtypesHostRecordDTO,
): string {
return '';
export function hostInitialLogTracesFilter(
host: HostData,
dotMetricsEnabled: boolean,
): TagFilterItem[] {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return [createFilterItem(hostKey, host.hostName || '')];
}
export const hostGetEntityName = (
host: InframonitoringtypesHostRecordDTO,
): string => host.hostName;
export function hostInitialEventsFilter(_host: HostData): TagFilterItem[] {
return [];
}
export const hostGetEntityName = (host: HostData): string => host.hostName;
export async function fetchHostListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): Promise<{
data: HostData[];
total: number;
error?: string | null;
rawData?: unknown;
}> {
const baseQuery = getHostListsQuery();
const payload: HostListPayload = {
...baseQuery,
limit: filters.limit,
offset: filters.offset,
filters: filters.filters ?? { items: [], op: 'and' },
orderBy: filters.orderBy,
start: filters.start,
end: filters.end,
groupBy: filters.groupBy ?? [],
};
const response = await getHostLists(payload, signal);
return {
data: response.payload?.data?.records || [],
total: response.payload?.data?.total || 0,
error: response.error,
rawData: response.payload?.data,
};
}
export async function fetchHostEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: HostData | null; error?: string | null }> {
const response = await getHostLists(
{
...getHostListsQuery(),
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
groupBy: [],
},
signal,
);
const records = response.payload?.data?.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
}

View File

@@ -1,91 +1,55 @@
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 { Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { HostData } from 'api/infraMonitoring/getHostLists';
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 { 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';
import { Container, Info } from '@signozhq/icons';
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>;
} {
function hostRowSource(host: HostData): { meta: Record<string, string> } {
return {
meta: {
...(host.meta ?? {}),
[INFRA_MONITORING_ATTR_KEYS.HOST_NAME]: host.hostName ?? '',
host_name: host.hostName ?? '',
'host.name': host.hostName ?? '',
os_type: host.os ?? '',
'os.type': host.os ?? '',
},
};
}
export function getHostRowKey(host: InframonitoringtypesHostRecordDTO): string {
export function getHostRowKey(host: HostData): string {
return host.hostName || 'unknown';
}
export function getHostItemKey(
host: InframonitoringtypesHostRecordDTO,
): string {
export function getHostItemKey(host: HostData): string {
return host.hostName ?? '';
}
function HostGroupCell({
row,
}: {
row: InframonitoringtypesHostRecordDTO;
}): JSX.Element {
function HostGroupCell({ row }: { row: HostData }): 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[] = [
export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'hostGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Host Group" />,
header: (): React.ReactNode => <EntityGroupHeader title="HOST GROUP" />,
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -100,11 +64,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'hostName',
header: (): React.ReactNode => (
<EntityGroupHeader
title="Hostname"
icon={<Container size={14} />}
docPath="/infrastructure-monitoring/host-monitoring#hostname"
/>
<EntityGroupHeader title="Hostname" icon={<Container size={14} />} />
),
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
@@ -118,48 +78,27 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
),
},
{
id: 'status',
id: 'active',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Sent system metrics in last 10 mins."
docPath="/infrastructure-monitoring/host-monitoring#status"
>
<div className={styles.statusHeader}>
Status
</ColumnHeader>
<Tooltip title="Sent system metrics in last 10 mins">
<Info size="md" />
</Tooltip>
</div>
),
accessorFn: (row): string => row.status,
width: { min: 140 },
accessorFn: (row): boolean => row.active,
width: { min: 150, default: 150 },
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[''];
cell: ({ value }): React.ReactNode => {
const active = value as boolean;
return (
<Badge
variant="outline"
color={statusDetails.color}
className={`${styles.statusTag}`}
className={`${styles.statusTag} ${
active ? styles.statusTagActive : styles.statusTagInactive
}`}
>
{statusDetails.label}
{active ? 'ACTIVE' : 'INACTIVE'}
</Badge>
);
},
@@ -167,9 +106,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
CPU Usage
</ColumnHeader>
<div className={styles.columnHeaderRight}>CPU Usage</div>
),
accessorFn: (row): number => row.cpu,
width: { min: 220 },
@@ -192,16 +129,15 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
className={styles.memoryUsageHeader}
>
Memory Usage (WSS)
</ColumnHeader>
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
Memory Usage
<Tooltip title="Excluding cache memory">
<Info size="md" />
</Tooltip>
</div>
),
accessorFn: (row): number => row.memory,
width: { min: 240 },
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -221,12 +157,10 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'wait',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#iowait">
IOWait
</ColumnHeader>
<div className={styles.columnHeaderRight}>IOWait</div>
),
accessorFn: (row): number => row.wait,
width: { min: 120 },
width: { min: 100, default: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const wait = value as number;
@@ -237,9 +171,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
entity={InfraMonitoringEntity.HOSTS}
attribute="IOWait metric"
>
<TanStackTable.Text>
{`${Number((wait * 100).toFixed(1))}%`}
</TanStackTable.Text>
<TanStackTable.Text>{`${Number((wait * 100).toFixed(1))}%`}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
@@ -247,15 +179,13 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'load15',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg (15min)
</ColumnHeader>
<div className={styles.columnHeaderRight}>Load Avg</div>
),
accessorFn: (row): number => row.load15,
width: { min: 200 },
width: { min: 100, default: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = Number(value);
const load15 = value as number;
return (
<ValidateColumnValueWrapper
@@ -263,35 +193,9 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
entity={InfraMonitoringEntity.HOSTS}
attribute="load average metric"
>
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{load15}</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

@@ -9,6 +9,13 @@
display: block;
}
.statusHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
width: 100%;
}
.columnHeaderRight {
text-align: right;
}

View File

@@ -2,16 +2,14 @@ import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
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';
@@ -23,15 +21,13 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
return <div className="hostname-column-value">{hostName}</div>;
}
return (
<>
<Typography.Text color="muted">-</Typography.Text>
<div className="hostname-cell-missing">
<Typography.Text color="muted" className="hostname-cell-placeholder">
-
</Typography.Text>
<Tooltip
title={
<div>
@@ -64,16 +60,52 @@ export function HostnameCell({
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
</span>
</Tooltip>
</>
</div>
);
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],
op: 'and',
},
groupBy: [],
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Host Name',
attributeKey: {
key: 'host_name',
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: 'system_cpu_load_average_15m',
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'OS Type',
attributeKey: {
key: 'os_type',
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: 'system_cpu_load_average_15m',
dataSource: DataSource.METRICS,
defaultOpen: true,
},
];
export function getHostsQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {
const hostNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
const metricName = dotMetricsEnabled
? 'system.cpu.load_average.15m'

View File

@@ -1,16 +0,0 @@
.columnHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.infoIcon {
display: inline-flex;
align-items: center;
cursor: pointer;
}
.columnHeaderLabel {
text-align: center;
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) 0px;
}

View File

@@ -1,90 +0,0 @@
import { Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './ColumnHeader.module.scss';
import cx from 'classnames';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface ColumnHeaderProps {
children?: React.ReactNode;
title?: string;
docPath?: string;
tooltip?: string;
className?: string;
}
function ColumnHeader({
children,
title,
docPath,
tooltip,
className,
}: ColumnHeaderProps): JSX.Element {
const renderContent = (): React.ReactNode => {
if (children) {
return children;
}
if (title) {
const parts = title.split('\n');
return parts.map((part, index) => (
<div key={`${part}-${index}`}>
{part}
{index < parts.length - 1 && <br />}
</div>
));
}
return null;
};
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this means?';
return (
<TooltipSimple
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<div className={styles.infoIcon}>
<Info size="md" />
</div>
</TooltipSimple>
);
}
if (tooltip) {
return (
<TooltipSimple title={tooltip}>
<div className={styles.infoIcon}>
<Info size="md" />
</div>
</TooltipSimple>
);
}
return null;
};
return (
<div className={cx(styles.columnHeader, className)} data-slot="column-header">
<div className={styles.columnHeaderLabel}>{renderContent()}</div>
{renderInfoIcon()}
</div>
);
}
export default ColumnHeader;

View File

@@ -2,16 +2,4 @@
display: flex;
align-items: center;
gap: var(--spacing-5);
padding-left: 4px;
}
.infoIcon {
display: inline-flex;
align-items: center;
color: var(--text-slate-secondary);
cursor: pointer;
&:hover {
color: var(--text-slate-primary);
}
}

View File

@@ -1,70 +1,19 @@
import { Group, Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Group } from '@signozhq/icons';
import styles from './EntityGroupHeader.module.scss';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface EntityGroupHeaderProps {
title: string;
icon?: React.ReactNode;
docPath?: string;
tooltip?: string;
}
function EntityGroupHeader({
title,
icon,
docPath,
tooltip,
}: EntityGroupHeaderProps): JSX.Element {
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this means?';
return (
<TooltipSimple
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<span className={styles.infoIcon}>
<Info size="md" />
</span>
</TooltipSimple>
);
}
if (tooltip) {
return (
<TooltipSimple title={tooltip}>
<span className={styles.infoIcon}>
<Info size="md" />
</span>
</TooltipSimple>
);
}
return null;
};
return (
<div className={styles.entityGroupHeader} data-slot="entity-group-header">
<span data-slot="icon">
{icon || <Group size={14} data-hide-expanded="true" />}
</span>{' '}
{title}
{renderInfoIcon()}
<div className={styles.entityGroupHeader}>
{icon || <Group size={14} data-hide-expanded="true" />} {title}
</div>
);
}

View File

@@ -6,6 +6,7 @@ import React, {
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
@@ -13,6 +14,7 @@ import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
@@ -38,11 +40,17 @@ import {
} from '@signozhq/icons';
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
@@ -72,7 +80,7 @@ export interface K8sDetailsMetadataConfig<T> {
}
export interface K8sDetailsFilters {
filter: { expression: string };
filters: TagFilter;
start: number;
end: number;
}
@@ -81,15 +89,15 @@ export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (selectedItem: string) => string;
getSelectedItemFilters: (selectedItem: string) => TagFilter;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
getInitialEventsFilters: (entity: T) => TagFilterItem[];
metadataConfig: K8sDetailsMetadataConfig<T>[];
entityWidgetInfo: {
title: string;
@@ -126,15 +134,33 @@ export interface K8sBaseDetailsProps<T> {
}>;
}
export function createFilterItem(
key: string,
value: string,
dataType: DataTypes = DataTypes.String,
): TagFilterItem {
return {
id: uuidv4(),
key: {
key,
dataType,
type: 'resource',
id: `${key}--string--resource--false`,
},
op: '=',
value,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetails<T>({
category,
eventCategory,
getSelectedItemExpression,
getSelectedItemFilters,
fetchEntityData,
getEntityName,
getInitialLogTracesExpression,
getInitialEventsExpression,
getInitialLogTracesFilters,
getInitialEventsFilters,
metadataConfig,
entityWidgetInfo,
getEntityQueryPayload,
@@ -175,12 +201,17 @@ export default function K8sBaseDetails<T>({
if (!selectedItem) {
return { data: null };
}
const filters = getSelectedItemFilters(selectedItem);
const { minTime, maxTime } = getMinMaxTime();
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const expression = getSelectedItemExpression(selectedItem);
return fetchEntityData({ filter: { expression }, start, end }, signal);
return fetchEntityData(
{
filters,
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
},
signal,
);
},
enabled: !!selectedItem,
});
@@ -192,15 +223,23 @@ export default function K8sBaseDetails<T>({
if (!entity) {
return '';
}
return getInitialLogTracesExpression(entity);
}, [entity, getInitialLogTracesExpression]);
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialLogTracesFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialLogTracesFilters]);
const eventsInitialExpression = useMemo(() => {
if (!entity) {
return '';
}
return getInitialEventsExpression(entity);
}, [entity, getInitialEventsExpression]);
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialEventsFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialEventsFilters]);
const handleClose = useCallback((): void => {
setSelectedItem(null);

View File

@@ -1,10 +1,3 @@
.tableContainer {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.emptyStateContainer {
display: flex;
flex: 1;
@@ -14,6 +7,7 @@
}
.k8SListTable {
padding-left: var(--spacing-2);
--tanstack-table-header-cell-bg: var(--l2-background);
--tanstack-table-header-cell-color: var(--l2-foreground);
--tanstack-table-cell-bg: var(--l2-background);
@@ -24,12 +18,9 @@
--tanstack-table-resize-handle-hover-bg: var(--l2-border);
--tanstack-table-row-height: 42px;
--tanstack-table-header-label-width: 100%;
--tanstack-cell-padding-top-override: 5px;
--tanstack-cell-padding-bottom-override: 5px;
--tanstack-cell-header-padding-left-override: 5px;
--tanstack-cell-padding-left-override: 26px;
--tanstack-cell-padding-left-override: 5px;
--tanstack-cell-padding-right-override: 5px;
--tanstack-expansion-first-col-padding-left: 30px;

View File

@@ -1,19 +1,15 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useQuery } from 'react-query';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import TanStackTable, {
TableColumnDef,
useCalculatedPageSize,
useHiddenColumnIds,
useTableParams,
} from 'components/TanStackTableView';
import { InfraMonitoringEvents } from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { parseAsString, useQueryState } from 'nuqs';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { openInNewTab } from 'utils/navigation';
import {
@@ -21,15 +17,15 @@ import {
InfraMonitoringEntity,
} from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringStatusFilter,
useInfraMonitoringPageListing,
useInfraMonitoringPageSizeListing,
} from '../hooks';
import { useInfraMonitoringLineClamp } from '../components';
import { K8sEmptyState } from './K8sEmptyState';
import { K8sExpandedRow } from './K8sExpandedRow';
import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
@@ -42,30 +38,24 @@ export type K8sBaseListEmptyStateContext = {
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
endTimeBeforeRetention?: boolean;
rawData?: unknown;
};
/** Base type constraint for K8s entity data */
export type K8sEntityData = { meta?: Record<string, string> | null };
export type K8sEntityData = { meta?: Record<string, string> };
export type K8sBaseListProps<T extends K8sEntityData> = {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
fetchListData: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
type?: 'list' | 'grouped_list';
records?: T[];
data?: T[];
data: T[];
total: number;
error?: string | null;
rawData?: unknown;
endTimeBeforeRetention?: boolean;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
@@ -75,12 +65,10 @@ export type K8sBaseListProps<T extends K8sEntityData> = {
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
) => React.ReactNode | null;
extraQueryKeyParts?: string[];
};
export function K8sBaseList<T extends K8sEntityData>({
controlListPrefix,
leftFilters,
entity,
tableColumns,
fetchListData,
@@ -88,14 +76,12 @@ export function K8sBaseList<T extends K8sEntityData>({
getItemKey,
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
}: K8sBaseListProps<T>): JSX.Element {
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
const lineClamp = useInfraMonitoringLineClamp();
const [queryFilters] = useInfraMonitoringFiltersK8s();
const [currentPage] = useInfraMonitoringPageListing();
const [currentPageSize] = useInfraMonitoringPageSizeListing();
const [groupBy] = useInfraMonitoringGroupBy();
const [orderBy] = useInfraMonitoringOrderBy();
const [statusFilter] = useInfraMonitoringStatusFilter();
const [selectedItem, setSelectedItem] = useQueryState(
'selectedItem',
parseAsString,
@@ -104,27 +90,6 @@ export function K8sBaseList<T extends K8sEntityData>({
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
rowHeight: 42,
});
const {
page: currentPage,
limit: currentPageSize,
setLimit,
} = useTableParams(
{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
},
{
page: 1,
limit: 10,
storageKey: `k8s-${entity}`,
calculatedPageSize,
},
);
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
@@ -140,11 +105,9 @@ export function K8sBaseList<T extends K8sEntityData>({
entity,
String(currentPageSize),
String(currentPage),
expression || '',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
statusFilter,
...extraQueryKeyParts,
);
}, [
getAutoRefreshQueryKey,
@@ -152,64 +115,35 @@ export function K8sBaseList<T extends K8sEntityData>({
entity,
currentPageSize,
currentPage,
expression,
queryFilters,
orderBy,
groupBy,
statusFilter,
extraQueryKeyParts,
]);
const queryClient = useQueryClient();
const { data, isLoading, isFetching, isError } = useQuery({
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: async ({ signal }) => {
queryFn: ({ signal }) => {
const { minTime, maxTime } = getMinMaxTime();
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const response = await fetchListData(
return fetchListData(
{
filter: {
expression: expression || '',
filterByStatus:
statusFilter === 'active' || statusFilter === 'inactive'
? statusFilter
: undefined,
},
groupBy:
groupBy && groupBy.length > 0
? groupBy.map((g) => ({ name: g }))
: undefined,
offset: (currentPage - 1) * currentPageSize,
limit: currentPageSize,
start,
end,
orderBy: orderBy
? { key: { name: orderBy.columnName }, direction: orderBy.order }
: undefined,
offset: (currentPage - 1) * currentPageSize,
filters: queryFilters || { items: [], op: 'AND' },
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
orderBy: orderBy || undefined,
groupBy: groupBy?.length > 0 ? groupBy : undefined,
},
signal,
);
return {
data: response.records || response.data || [],
total: response.total,
error: response.error,
endTimeBeforeRetention: response.endTimeBeforeRetention,
rawData: response.rawData ?? response,
warning: response.warning ?? null,
};
},
refetchInterval: isRefreshEnabled ? refreshInterval : false,
});
const cancelQuery = useCallback((): void => {
void queryClient.cancelQueries({ queryKey });
}, [queryClient, queryKey]);
const pageData = data?.data ?? [];
const totalCount = data?.total || 0;
const hasFilters = !!expression?.trim();
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
const getGroupKeyFn = useCallback(
(item: T) => getGroupedByMeta(item, groupBy),
@@ -217,7 +151,7 @@ export function K8sBaseList<T extends K8sEntityData>({
);
useEffect(() => {
void logEvent(InfraMonitoringEvents.PageVisited, {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -228,10 +162,10 @@ export function K8sBaseList<T extends K8sEntityData>({
const handleRowClick = useCallback(
(_record: T, itemKey: string): void => {
if (groupBy.length === 0) {
void setSelectedItem(itemKey);
setSelectedItem(itemKey);
}
void logEvent(InfraMonitoringEvents.ItemClicked, {
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -251,7 +185,7 @@ export function K8sBaseList<T extends K8sEntityData>({
url.searchParams.set('selectedItem', itemKey);
openInNewTab(url.pathname + url.search);
void logEvent(InfraMonitoringEvents.ItemClicked, {
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -280,19 +214,11 @@ export function K8sBaseList<T extends K8sEntityData>({
entity={entity}
tableColumns={expandedRowColumns}
fetchListData={fetchListData}
extraQueryKeyParts={extraQueryKeyParts}
getRowKey={getRowKey}
getItemKey={getItemKey}
/>
),
[
entity,
fetchListData,
getRowKey,
getItemKey,
expandedRowColumns,
extraQueryKeyParts,
],
[entity, fetchListData, getRowKey, getItemKey, expandedRowColumns],
);
const getRowCanExpand = useCallback(
@@ -308,78 +234,64 @@ export function K8sBaseList<T extends K8sEntityData>({
totalCount,
hasFilters,
isLoading: showTableLoadingState,
endTimeBeforeRetention: data?.endTimeBeforeRetention,
rawData: data?.rawData,
}) || (
<K8sEmptyState
isError={isError}
error={data?.error}
isLoading={showTableLoadingState}
endTimeBeforeRetention={data?.endTimeBeforeRetention}
rawData={data?.rawData}
/>
);
const showEmptyState = !showTableLoadingState && pageData.length === 0;
const paginationWarningContent = data?.warning ? (
<K8sPaginationWarning warning={data.warning} />
) : null;
return (
<>
<K8sHeader
controlListPrefix={controlListPrefix}
leftFilters={leftFilters}
entity={entity}
showAutoRefresh={!selectedItem}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isFetching={isFetching}
cancelQuery={cancelQuery}
/>
<div ref={containerRef} className={styles.tableContainer}>
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
</Typography>
)}
{isError && (
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
)}
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isLoading={showTableLoadingState}
getRowKey={getRowKey}
getItemKey={getItemKey}
groupBy={groupBy.map((g) => ({ key: g }))}
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
}}
pagination={{
total: totalCount,
showTotalCount: true,
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
calculatedPageSize,
onLimitChange: setLimit,
}}
plainTextCellLineClamp={lineClamp}
prefixPaginationContent={paginationWarningContent}
paginationClassname={styles.paginationContainer}
/>
)}
</div>
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isLoading={showTableLoadingState}
getRowKey={getRowKey}
getItemKey={getItemKey}
groupBy={groupBy}
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
}}
pagination={{
total: totalCount,
defaultLimit: 10,
defaultPage: 1,
showTotalCount: true,
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
}}
paginationClassname={styles.paginationContainer}
/>
)}
</>
);
}

View File

@@ -11,6 +11,12 @@ import type { K8sBaseListEmptyStateContext } from './K8sBaseList';
import styles from './K8sEmptyState.module.scss';
export interface K8sListResponseMetadata {
sentAnyHostMetricsData?: boolean;
isSendingK8SAgentMetrics?: boolean;
endTimeBeforeRetention?: boolean;
}
type K8sEmptyStateProps = Partial<K8sBaseListEmptyStateContext>;
const handleContactSupport = (isCloudUser: boolean): void => {
@@ -25,7 +31,7 @@ export function K8sEmptyState({
isError,
error,
isLoading,
endTimeBeforeRetention,
rawData,
}: K8sEmptyStateProps): JSX.Element | null {
const { isCloudUser } = useGetTenantLicense();
@@ -64,7 +70,47 @@ export function K8sEmptyState({
);
}
if (endTimeBeforeRetention) {
const metadata = rawData as K8sListResponseMetadata | undefined;
if (metadata?.sentAnyHostMetricsData === false) {
return (
<div className={styles.container}>
<div className={styles.content}>
<img className={styles.eyesEmoji} src={eyesEmojiUrl} alt="eyes emoji" />
<div className={styles.noDataMessage}>
<h5 className={styles.title}>No host metrics data received yet</h5>
<span className={styles.message}>
Please refer to{' '}
<a
href="https://signoz.io/docs/infrastructure-monitoring/hostmetrics/"
target="_blank"
rel="noreferrer"
>
our documentation
</a>{' '}
to learn how to send host metrics.
</span>
</div>
</div>
</div>
);
}
if (metadata?.isSendingK8SAgentMetrics) {
return (
<div className={styles.container}>
<div className={styles.content}>
<img className={styles.eyesEmoji} src={eyesEmojiUrl} alt="eyes emoji" />
<span className={styles.message}>
To see K8s metrics, upgrade to the latest version of SigNoz k8s-infra
chart. Please contact support if you need help.
</span>
</div>
</div>
);
}
if (metadata?.endTimeBeforeRetention) {
return (
<div className={styles.container}>
<div className={styles.content}>

View File

@@ -1,7 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import TanStackTable, {
@@ -9,19 +7,16 @@ import TanStackTable, {
TableColumnDef,
TanStackTableStateProvider,
} from 'components/TanStackTableView';
import { QueryParams } from 'constants/query';
import { CornerDownRight } from '@signozhq/icons';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { v4 as uuid } from 'uuid';
import { useQueryState } from 'nuqs';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { InfraMonitoringEntity } from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringPageListing,
@@ -30,7 +25,6 @@ import {
import { K8sBaseFilters } from './types';
import styles from './K8sExpandedRow.module.scss';
import { buildExpressionFromGroupMeta } from './utils';
const EXPANDED_ROW_LIMIT = 10;
@@ -41,21 +35,15 @@ export type K8sExpandedRowProps<T> = {
groupMeta?: Record<string, string>;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
/** API fetch function for expanded row data */
fetchListData?: (
fetchListData: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
type?: 'list' | 'grouped_list';
records?: T[];
data?: T[];
data: T[];
total: number;
error?: string | null;
rawData?: unknown;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Extra parts to include in the react-query cache key (e.g., status filter). */
extraQueryKeyParts?: string[];
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
@@ -68,20 +56,14 @@ export function K8sExpandedRow<T>({
entity,
tableColumns,
fetchListData,
extraQueryKeyParts = [],
getRowKey,
getItemKey,
}: K8sExpandedRowProps<T>): JSX.Element {
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
const parentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const [queryFilters, setFilters] = useInfraMonitoringFiltersK8s();
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const location = useLocation();
const orderByParamKey = useMemo(
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
@@ -103,10 +85,35 @@ export function K8sExpandedRow<T>({
const storageKey = `k8s-${entity}-columns-expanded`;
const expressionForRecord = useMemo(
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
[parentExpression, groupMeta],
);
const createFiltersForRecord = useCallback((): NonNullable<
IBuilderQuery['filters']
> => {
const baseFilters: IBuilderQuery['filters'] = {
items: [...(queryFilters?.items || [])],
op: 'and',
};
const metaKeys = groupMeta ?? {};
for (const key of Object.keys(metaKeys)) {
const value = metaKeys[key];
// Skip empty values to avoid creating invalid filters
if (value === '' || value === undefined || value === null) {
continue;
}
baseFilters.items.push({
key: {
key,
type: 'resource',
},
op: '=',
value,
id: key,
});
}
return baseFilters;
}, [queryFilters?.items, groupMeta]);
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
@@ -123,9 +130,8 @@ export function K8sExpandedRow<T>({
'k8sExpandedRow',
JSON.stringify(groupMeta),
rowKey,
expressionForRecord,
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
...extraQueryKeyParts,
);
}, [
getAutoRefreshQueryKey,
@@ -133,83 +139,49 @@ export function K8sExpandedRow<T>({
entity,
groupMeta,
rowKey,
expressionForRecord,
queryFilters,
orderBy,
extraQueryKeyParts,
]);
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: async ({ signal }) => {
if (!fetchListData) {
return { data: [] as T[], total: 0 };
}
const { minTime, maxTime } = getMinMaxTime();
const response = await fetchListData(
return await fetchListData(
{
filter: { expression: expressionForRecord },
limit: EXPANDED_ROW_LIMIT,
offset: 0,
filters: createFiltersForRecord(),
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
orderBy: orderBy
? { key: { name: orderBy.columnName }, direction: orderBy.order }
: undefined,
orderBy: orderBy || undefined,
groupBy: undefined,
},
signal,
);
return {
data: response.records || response.data || [],
total: response.total,
error: response.error,
};
},
staleTime: 1000 * 60 * 30,
refetchInterval: isRefreshEnabled ? refreshInterval : false,
enabled: !!fetchListData,
});
const expandedData = data?.data ?? [];
const handleRowClick = useCallback(
(_row: T, itemKey: string): void => {
void setSelectedItem(itemKey);
setSelectedItem(itemKey);
},
[setSelectedItem],
);
const handleViewAllClick = (): void => {
void setGroupBy([]);
void setCurrentPage(1);
const filters = createFiltersForRecord();
setGroupBy([]);
setCurrentPage(1);
setFilters(filters);
if (orderBy) {
void setMainOrderBy(orderBy);
setMainOrderBy(orderBy);
}
const updatedQuery = {
...currentQuery,
id: uuid(),
builder: {
...currentQuery.builder,
queryData: [
{
...(currentQuery.builder.queryData[0] || {}),
filter: { expression: expressionForRecord },
filters: { items: [], op: 'AND' as const },
},
],
},
};
const newUrlQuery = new URLSearchParams(urlQuery.toString());
newUrlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
};
const total = data?.total ?? 0;

View File

@@ -24,37 +24,11 @@
.columnsList {
display: flex;
flex-direction: column;
[data-slot='icon'] {
display: none;
}
[data-slot='entity-group-header'] {
padding-left: 0;
}
[data-slot='column-header'] {
display: flex;
width: auto;
span {
padding: 0;
text-align: left;
}
br {
display: none;
}
}
}
.columnItem {
justify-content: flex-start !important;
width: 100%;
> span {
width: auto !important;
}
}
.horizontalDivider {

View File

@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react';
import { useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import {
@@ -12,7 +12,7 @@ import styles from './K8sFiltersSidePanel.module.scss';
type ColumnPickerItem = {
id: string;
label: ReactNode;
label: string;
canBeHidden: boolean;
visibilityBehavior:
| 'hidden-on-expand'
@@ -20,16 +20,15 @@ type ColumnPickerItem = {
| 'always-visible';
};
function renderHeader(header: string | (() => ReactNode)): ReactNode {
return typeof header === 'function' ? header() : header;
}
/**
* Converts TableColumnDef to column picker item format
*/
function toColumnPickerItems<T>(
columns: TableColumnDef<T>[],
): ColumnPickerItem[] {
return columns.map((col) => ({
id: col.id,
label: renderHeader(col.header),
label: typeof col.header === 'string' ? col.header : col.id,
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
}));

View File

@@ -2,7 +2,7 @@ import { getGroupByEl } from './utils';
import { useInfraMonitoringGroupBy } from '../hooks';
interface K8sEntityWithMeta {
meta?: Record<string, string> | null;
meta?: Record<string, string>;
}
function K8sGroupCell<T extends K8sEntityWithMeta>({

View File

@@ -1,14 +1,13 @@
.k8SListControls {
padding: var(--spacing-4);
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
align-items: center;
gap: var(--spacing-4);
width: 100%;
:global(.ant-select-selector) {
border-radius: 2px;
border: 1px solid var(--l2-border) !important;
border: 1px solid var(--border) !important;
background-color: var(--l2-background) !important;
input {
@@ -21,81 +20,28 @@
}
}
.k8SListControlsRow {
.k8SListControlsLeft {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-4);
width: 100%;
align-items: center;
justify-content: flex-end;
}
.k8SQbSearchContainer {
width: 100%;
}
.k8SFiltersGroupByRow {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-4);
min-width: 0;
@media (max-width: 1600px) {
flex: 1 1 100%;
order: 9;
.k8SQbSearchContainer {
flex: 1;
min-width: 240px;
max-width: 60%;
}
}
.k8SAttributeSearchContainer {
flex: 1 1 240px;
max-width: 400px;
flex: 1;
min-width: 240px;
max-width: 40%;
display: flex;
align-items: center;
}
.k8SRunButton {
flex: 0 0 auto;
}
.k8SFiltersButton {
flex: 0 0 auto;
}
.k8SDateTimeSelection {
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
--date-time-selector-border-color: var(--l2-border);
@media (max-width: 1200px) {
flex: 1 1 100%;
order: 10;
}
:global(.timeSelection-input) {
height: 32px;
}
:global(.ant-input-affix-wrapper) {
border-color: var(--l2-border);
&:hover {
border-color: var(--l2-border) !important;
}
}
:global(.refresh-actions .ant-form-item-control-input) {
&,
:global(.ant-btn) {
height: 30px;
min-height: unset;
}
}
}
.groupByLabel {
min-width: max-content;
font-size: var(--periscope-font-size-base, 13px);
@@ -104,7 +50,7 @@
letter-spacing: -0.07px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
border: 1px solid var(--border);
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
@@ -118,8 +64,6 @@
}
.groupBySelect {
width: 100%;
:global(.ant-select-selector) {
border-left: none;
border-top-left-radius: 0px;

View File

@@ -1,215 +1,167 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import React, { useCallback, useMemo, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Select } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TableColumnDef } from 'components/TanStackTableView';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { FeatureKeys } from 'constants/features';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { SlidersHorizontal } from '@signozhq/icons';
import { v4 as uuid } from 'uuid';
import {
useGlobalTimeQueryInvalidate,
useGlobalTimeStore,
} from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { useAppContext } from 'providers/App/App';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
ENTITY_FILTER_PLACEHOLDERS,
GetK8sEntityToAggregateAttribute,
InfraMonitoringEntity,
METRIC_NAMESPACE_BY_ENTITY,
} from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringPageListing,
} from '../hooks';
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
import styles from './K8sHeader.module.scss';
import { TooltipSimple } from '@signozhq/ui/tooltip';
interface K8sHeaderProps<TData> {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
showAutoRefresh: boolean;
columns: TableColumnDef<TData>[];
columnStorageKey: string;
isFetching?: boolean;
cancelQuery: () => void;
}
function K8sHeader<TData>({
controlListPrefix,
leftFilters,
entity,
showAutoRefresh,
columns,
columnStorageKey,
isFetching = false,
cancelQuery,
}: K8sHeaderProps<TData>): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
// null = user never touched the search box; fall back to the current
// query expression on Run so an untouched search doesn't wipe filters
const stagedExpressionRef = useRef<string | null>(null);
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const location = useLocation();
const invalidateQueries = useGlobalTimeQueryInvalidate();
const currentQuery = initialQueriesMap[DataSource.METRICS];
const queryData = useMemo(
(): IBuilderQuery => ({
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
}),
[currentQuery],
const updatedCurrentQuery = useMemo(() => {
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
filters = urlFilters;
}
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters,
},
],
},
};
}, [currentQuery, urlFilters]);
const query = useMemo(
() => updatedCurrentQuery?.builder?.queryData[0] || null,
[updatedCurrentQuery],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const [, setCurrentPage] = useInfraMonitoringPageListing();
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setUrlFilters(value || null);
handleChangeQueryData('filters', value);
setCurrentPage(1);
const handleRunQuery = useCallback(
(newExpression?: string): void => {
const currentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const finalExpression = newExpression ?? currentExpression;
void setCurrentPage(1);
const updatedQuery = {
...currentQuery,
id: uuid(),
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((query, idx) =>
idx === 0
? {
...query,
filter: { expression: finalExpression || '' },
filters: { items: [], op: 'AND' as const },
}
: query,
),
},
};
// Use window.location.search to get fresh URL params (avoids stale hook state)
const newUrlQuery = new URLSearchParams(window.location.search);
newUrlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
void invalidateQueries();
if (finalExpression?.trim()) {
void logEvent(InfraMonitoringEvents.FilterApplied, {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: entity,
category: InfraMonitoringEvents.Pod,
});
}
},
[
currentQuery,
safeNavigate,
location.pathname,
setCurrentPage,
entity,
invalidateQueries,
],
[handleChangeQueryData, setCurrentPage, setUrlFilters],
);
const handleStageRunQuery = useCallback((): void => {
handleRunQuery(stagedExpressionRef.current ?? undefined);
}, [handleRunQuery]);
const handleExpressionChange = useCallback((value: string): void => {
stagedExpressionRef.current = value;
}, []);
const handleCancelQuery = useCallback((): void => {
cancelQuery();
}, [cancelQuery]);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const { minTime, maxTime } = getMinMaxTime();
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters } =
useGetFieldsKeys(
useGetAggregateKeys(
{
signal: TelemetrytypesSignalDTO.metrics,
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
limit: 100,
startUnixMilli,
endUnixMilli,
fieldContext: TelemetrytypesFieldContextDTO.resource,
// the search text is intentionally not included
// searchText: expression,
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute: GetK8sEntityToAggregateAttribute(
entity,
dotMetricsEnabled,
),
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
query: {
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
},
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
entity,
);
const flatFieldKeys = useMemo(() => {
const keys = groupByFiltersData?.data?.keys;
if (!keys) {
return [];
}
const allKeys = Object.values(keys).flat();
const seen = new Set<string>();
return allKeys.filter((field) => {
if (seen.has(field.name)) {
return false;
}
seen.add(field.name);
return true;
});
}, [groupByFiltersData]);
const groupByOptions = useMemo(
() =>
flatFieldKeys.map((field) => ({
value: field.name,
label: field.name,
})),
[flatFieldKeys],
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
[groupByFiltersData],
);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const handleGroupByChange = useCallback(
(value: string[]) => {
void setCurrentPage(1);
void setGroupBy(value);
(value: IBuilderQuery['groupBy']) => {
const newGroupBy = [];
void logEvent(InfraMonitoringEvents.GroupByChanged, {
for (let index = 0; index < value.length; index++) {
const element = value[index] as unknown as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(k) => k.key === element,
);
if (key) {
newGroupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(newGroupBy);
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Pod,
});
},
[setCurrentPage, setGroupBy],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
const onClickOutside = useCallback(() => {
@@ -218,69 +170,53 @@ function K8sHeader<TData>({
return (
<div className={styles.k8SListControls}>
<div className={styles.k8SListControlsRow}>
<div className={styles.k8SListControlsLeft}>
{controlListPrefix}
<div className={styles.k8SFiltersGroupByRow}>
{leftFilters}
<div className={styles.k8SAttributeSearchContainer}>
<div className={styles.groupByLabel}>Group by</div>
<Select
className={styles.groupBySelect}
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
</div>
<div className={styles.k8SDateTimeSelection}>
<DateTimeSelectionV2
showAutoRefresh={showAutoRefresh}
showRefreshText={false}
hideShareModal
<div className={styles.k8SQbSearchContainer}>
<QueryBuilderSearch
query={query as IBuilderQuery}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts
entity={entity}
/>
</div>
<RunQueryBtn
isLoadingQueries={isFetching}
onStageRunQuery={handleStageRunQuery}
handleCancelQuery={handleCancelQuery}
className={styles.k8SRunButton}
/>
<TooltipSimple title="Click to add more columns to this table">
<Button
type="button"
variant="outlined"
size="icon"
color="secondary"
data-testid="k8s-list-filters-button"
onClick={(): void => setIsFiltersSidePanelOpen(true)}
className={styles.k8SFiltersButton}
>
<SlidersHorizontal size={14} />
</Button>
</TooltipSimple>
<div className={styles.k8SAttributeSearchContainer}>
<div className={styles.groupByLabel}> Group by </div>
<Select
className={styles.groupBySelect}
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
style={{ width: '100%' }}
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
</div>
<div className={styles.k8SQbSearchContainer}>
<QuerySearch
queryData={queryData}
dataSource={DataSource.METRICS}
onChange={handleExpressionChange}
onRun={handleRunQuery}
signalSource=""
showFilterSuggestionsWithoutMetric
placeholder={ENTITY_FILTER_PLACEHOLDERS[entity]}
metricNamespace={METRIC_NAMESPACE_BY_ENTITY[entity]}
<div className={styles.k8SListControlsRight}>
<DateTimeSelectionV2
showAutoRefresh={showAutoRefresh}
showRefreshText={false}
hideShareModal
/>
<Button
type="button"
variant="ghost"
size="icon"
color="none"
data-testid="k8s-list-filters-button"
onClick={(): void => setIsFiltersSidePanelOpen(true)}
>
<SlidersHorizontal size={14} />
</Button>
</div>
<K8sFiltersSidePanel

View File

@@ -1,15 +0,0 @@
.paginationWarning {
border: 1px solid var(--callout-warning-border);
background-color: var(--callout-warning-background);
padding: var(--spacing-2) var(--spacing-4);
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-4);
}
.warningIcon {
cursor: pointer;
}

View File

@@ -1,37 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import styles from './K8sPaginationWarning.module.scss';
import TriangleAlert from '@signozhq/icons/TriangleAlert';
export type K8sPaginationWarningProps = {
warning: Querybuildertypesv5QueryWarnDataDTO;
};
export function K8sPaginationWarning({
warning,
}: K8sPaginationWarningProps): JSX.Element {
return (
<span data-testid="k8s-list-warning-popover">
<WarningPopover
warningData={{
code: 'WARNING',
message: warning.message ?? '',
url: warning.url ?? '',
warnings:
warning.warnings?.map((w) => ({ message: w.message ?? '' })) ?? [],
}}
>
<div className={styles.paginationWarning}>
Your data contains some warnings
<TriangleAlert
size={16}
className={styles.warningIcon}
color={Color.BG_AMBER_500}
/>
</div>
</WarningPopover>
</span>
);
}

View File

@@ -22,15 +22,6 @@ import { openInNewTab } from 'utils/navigation';
import { TableColumnDef } from 'components/TanStackTableView';
import { InfraMonitoringEntity } from '../../constants';
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
import { K8sBaseList, K8sBaseListProps, K8sEntityData } from '../K8sBaseList';
jest.mock('utils/navigation', () => ({
@@ -215,10 +206,8 @@ describe('K8sBaseList', () => {
const itemId2 = Math.random().toString(36).slice(7);
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithTitle>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>();
beforeEach(() => {
@@ -270,10 +259,7 @@ describe('K8sBaseList', () => {
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.limit).toBe(10);
expect(filters.offset).toBe(0);
expect(filters.filter).toStrictEqual({
expression: '',
filterByStatus: undefined,
});
expect(filters.filters).toStrictEqual({ items: [], op: 'AND' });
expect(filters.groupBy).toBeUndefined();
expect(filters.orderBy).toBeUndefined();
});
@@ -433,11 +419,11 @@ describe('K8sBaseList', () => {
});
});
describe('with URL params (orderBy, groupBy old format, pagination)', () => {
describe('with URL params (orderBy, groupBy, pagination)', () => {
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
const groupByValue = [
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
@@ -476,11 +462,8 @@ describe('K8sBaseList', () => {
});
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.orderBy).toStrictEqual({
key: { name: 'cpu' },
direction: 'desc',
});
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
expect(filters.orderBy).toStrictEqual({ columnName: 'cpu', order: 'desc' });
expect(filters.groupBy).toStrictEqual(groupByValue);
expect(filters.offset).toBe(20); // (3 - 1) * 10 = 20
expect(filters.limit).toBe(10);
});
@@ -504,84 +487,14 @@ describe('K8sBaseList', () => {
(c) => c[0].groupBy && c[0].groupBy.length > 0,
);
expect(callWithGroupBy).toBeDefined();
expect(callWithGroupBy?.[0].groupBy).toStrictEqual([
{ name: 'k8s.namespace.name' },
]);
});
});
describe('with URL params (groupBy new format - string array)', () => {
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
const groupByValue = ['k8s.namespace.name'];
beforeEach(() => {
onUrlUpdateMock.mockClear();
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [
{ id: 'namespace-default', meta: { 'k8s.namespace.name': 'default' } },
],
total: 50,
error: null,
});
renderComponent<TestItem>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
queryParams: {
orderBy: JSON.stringify({ columnName: 'cpu', order: 'desc' }),
groupBy: JSON.stringify(groupByValue),
page: '3',
},
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should call fetchListData with groupBy from new format URL', async () => {
await waitFor(() => {
expect(fetchListDataMock).toHaveBeenCalled();
});
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
});
it('should render expand icons when groupBy is set with new format', async () => {
await waitFor(() => {
expect(screen.getByText('namespace-default')).toBeInTheDocument();
});
const expandButtons = screen.getAllByRole('button');
expect(expandButtons.length).toBeGreaterThan(0);
});
it('should render data with new groupBy format', async () => {
await waitFor(() => {
expect(screen.getByText('namespace-default')).toBeInTheDocument();
});
const callWithGroupBy = fetchListDataMock.mock.calls.find(
(c) => c[0].groupBy && c[0].groupBy.length > 0,
);
expect(callWithGroupBy).toBeDefined();
expect(callWithGroupBy?.[0].groupBy).toStrictEqual([
{ name: 'k8s.namespace.name' },
]);
expect(callWithGroupBy?.[0].groupBy).toStrictEqual(groupByValue);
});
});
describe('with empty data', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
@@ -590,6 +503,10 @@ describe('K8sBaseList', () => {
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
},
});
renderComponent<TestItem>({
@@ -617,8 +534,8 @@ describe('K8sBaseList', () => {
describe('with error response', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
@@ -652,10 +569,10 @@ describe('K8sBaseList', () => {
});
});
describe('with end time before retention (endTimeBeforeRetention=true)', () => {
describe('with no metrics data (sentAnyHostMetricsData=false)', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
@@ -664,7 +581,96 @@ describe('K8sBaseList', () => {
data: [],
total: 0,
error: null,
endTimeBeforeRetention: true,
rawData: {
sentAnyHostMetricsData: false,
isSendingK8SAgentMetrics: false,
},
});
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should display no metrics data message', async () => {
await waitFor(() => {
expect(
screen.getByText(/No host metrics data received yet/i),
).toBeInTheDocument();
});
});
it('should display link to documentation', async () => {
await waitFor(() => {
const link = screen.getByRole('link', { name: /our documentation/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/',
);
});
});
});
describe('with incorrect K8s agent metrics (isSendingK8SAgentMetrics=true)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: true,
},
});
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should display upgrade message', async () => {
await waitFor(() => {
expect(
screen.getByText(/upgrade to the latest version of SigNoz k8s-infra/i),
).toBeInTheDocument();
});
});
});
describe('with end time before retention (endTimeBeforeRetention=true)', () => {
const fetchListDataMock = jest.fn<
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
endTimeBeforeRetention: true,
},
});
renderComponent<TestItem>({
@@ -691,8 +697,8 @@ describe('K8sBaseList', () => {
describe('column visibility based on TanStack columns', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
>();
beforeEach(() => {
@@ -730,10 +736,8 @@ describe('K8sBaseList', () => {
describe('column behavior with groupBy (expanded/collapsed)', () => {
const fetchListDataMock = jest.fn<
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithGroup>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
>();
beforeEach(() => {
@@ -803,8 +807,8 @@ describe('K8sBaseList', () => {
describe('column visibility in expanded row (nested table)', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
const groupByValue = [
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
@@ -848,8 +852,8 @@ describe('K8sBaseList', () => {
describe('TanStack table column rendering', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
>();
beforeEach(() => {
@@ -905,40 +909,4 @@ describe('K8sBaseList', () => {
});
});
});
describe('with warnings from API', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [{ id: 'item-1' }],
total: 1,
error: null,
warning: {
message: 'Some data may be incomplete',
url: 'https://docs.example.com/partial-data',
warnings: [{ message: 'Node xyz did not report metrics' }],
},
});
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
});
it('should render warning popover in pagination area', async () => {
await waitFor(() => {
expect(screen.getByTestId('k8s-list-warning-popover')).toBeInTheDocument();
});
});
});
});

View File

@@ -1,25 +1,16 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { OrderBySchemaType } from '../schemas';
export type K8sBaseFilters = {
filter: {
expression: string;
filterByStatus?: 'active' | 'inactive' | '';
};
groupBy?: Array<{ name: string }>;
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
start: number;
end: number;
orderBy?: {
key: { name: string };
direction: 'asc' | 'desc';
};
};
export type K8sListResponse<T> = {
type: 'list' | 'grouped_list';
records: T[];
total: number;
endTimeBeforeRetention?: boolean;
error?: string | null;
orderBy?: OrderBySchemaType;
};
/**

View File

@@ -1,8 +1,8 @@
import { Badge } from '@signozhq/ui/badge';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import styles from './utils.module.scss';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
const dotToUnder: Record<string, string> = {
'os.type': 'os_type',
@@ -22,13 +22,15 @@ const dotToUnder: Record<string, string> = {
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
};
export function getGroupedByMeta<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): Record<string, string> {
export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
itemData: T,
groupBy: BaseAutocompleteData[],
): Record<string, string> {
const result: Record<string, string> = {};
const meta = itemData.meta ?? {};
groupBy.forEach((rawKey) => {
groupBy.forEach((group) => {
const rawKey = group.key as string;
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
result[rawKey] = (meta[metaKey] || meta[rawKey]) ?? '';
});
@@ -36,13 +38,45 @@ export function getGroupedByMeta<
return result;
}
export function getGroupByEl<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): React.ReactNode {
export function getRowKey<T extends { meta?: Record<string, string> }>(
itemData: T,
getItemIdentifier: () => string,
groupBy: BaseAutocompleteData[],
): string {
const nodeIdentifier = getItemIdentifier();
const meta = itemData.meta ?? {};
if (groupBy.length === 0) {
return nodeIdentifier || JSON.stringify(meta);
}
const groupedMeta = getGroupedByMeta(itemData, groupBy);
const groupKey = Object.values(groupedMeta).join('-');
if (groupKey && nodeIdentifier) {
return `${groupKey}-${nodeIdentifier}`;
}
if (groupKey) {
return groupKey;
}
if (nodeIdentifier) {
return nodeIdentifier;
}
return JSON.stringify(meta);
}
export function getGroupByEl<T extends { meta?: Record<string, string> }>(
itemData: T,
groupBy: IBuilderQuery['groupBy'],
): React.ReactNode {
const groupByValues: string[] = [];
const meta = itemData.meta ?? {};
groupBy.forEach((rawKey) => {
groupBy.forEach((group) => {
const rawKey = group.key as string;
// Choose mapped key if present, otherwise use rawKey
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
const value = meta[metaKey] || meta[rawKey] || '<no-value>';
@@ -64,28 +98,3 @@ export function getGroupByEl<
</div>
);
}
export function buildExpressionFromGroupMeta(
parentExpression: string,
groupMeta: Record<string, string> | undefined,
): string {
const items: TagFilterItem[] = Object.entries(groupMeta ?? {})
.filter(([, value]) => value !== '' && value !== undefined && value !== null)
.map(([key, value]) => ({
key: { key, type: 'resource' },
op: '=',
value,
id: key,
}));
const metaExpression = convertFiltersToExpression({
items,
op: 'AND',
}).expression;
const parent = parentExpression?.trim();
if (parent && metaExpression) {
return `${parent} AND ${metaExpression}`;
}
return parent || metaExpression;
}

View File

@@ -1,24 +1,21 @@
import { useCallback } from 'react';
import { listClusters } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sClustersList, K8sClusterData } from './api';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsMetadataConfig,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
k8sClusterInitialLogTracesExpression,
k8sClusterGetSelectedItemFilters,
k8sClusterInitialEventsFilter,
k8sClusterInitialLogTracesFilter,
} from './constants';
import {
getK8sClusterItemKey,
@@ -31,93 +28,66 @@ function K8sClustersList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch clusters';
return {
type: 'list' as const,
records: [] as InframonitoringtypesClusterRecordDTO[],
total: 0,
error: errMsg,
};
}
const response = await getK8sClustersList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesClusterRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listClusters(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
): Promise<{ data: K8sClusterData | null; error?: string | null }> => {
const response = await getK8sClustersList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch cluster';
return {
data: null,
error: errMsg,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesClusterRecordDTO>
<K8sBaseList<K8sClusterData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.CLUSTERS}
tableColumns={k8sClustersColumnsConfig}
@@ -127,14 +97,14 @@ function K8sClustersList({
eventCategory={InfraMonitoringEvents.Cluster}
/>
<K8sBaseDetails<InframonitoringtypesClusterRecordDTO>
<K8sBaseDetails<K8sClusterData>
category={InfraMonitoringEntity.CLUSTERS}
eventCategory={InfraMonitoringEvents.Cluster}
getSelectedItemExpression={k8sClusterGetSelectedItemExpression}
getSelectedItemFilters={k8sClusterGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sClusterGetEntityName}
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
getInitialLogTracesFilters={k8sClusterInitialLogTracesFilter}
getInitialEventsFilters={k8sClusterInitialEventsFilter}
metadataConfig={k8sClusterDetailsMetadataConfig}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}

View File

@@ -0,0 +1,125 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sClustersListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sClusterData {
clusterUID: string;
cpuUsage: number;
cpuAllocatable: number;
memoryUsage: number;
memoryAllocatable: number;
meta: {
k8s_cluster_name: string;
k8s_cluster_uid: string;
};
}
export interface K8sClustersListResponse {
status: string;
data: {
type: string;
records: K8sClusterData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const clustersMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.cluster.uid', under: 'k8s_cluster_uid' },
] as const;
export function mapClustersMeta(
raw: Record<string, unknown>,
): K8sClusterData['meta'] {
const out: Record<string, unknown> = { ...raw };
clustersMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sClusterData['meta'];
}
export const getK8sClustersList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/clusters/list', requestProps, {
signal,
headers,
});
const payload: K8sClustersListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapClustersMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,39 +1,53 @@
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sClusterData } from './api';
export const k8sClusterGetSelectedItemExpression = (
export const k8sClusterGetSelectedItemFilters = (
selectedItemId: string,
): string => `k8s.cluster.name = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_cluster_name',
key: {
key: 'k8s_cluster_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sClusterData>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.meta.k8s_cluster_name }];
export const k8sClusterInitialEventsExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string => {
const objectName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Cluster' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
};
export const k8sClusterInitialEventsFilter = (
item: K8sClusterData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Cluster'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_cluster_name),
];
export const k8sClusterInitialLogTracesExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string => {
const clusterName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${clusterName}`;
};
export const k8sClusterInitialLogTracesFilter = (
item: K8sClusterData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
];
export const k8sClusterGetEntityName = (
item: InframonitoringtypesClusterRecordDTO,
): string => item.clusterName || '';
export const k8sClusterGetEntityName = (item: K8sClusterData): string =>
item.meta.k8s_cluster_name;
export const clusterWidgetInfo = [
{
@@ -71,7 +85,7 @@ export const clusterWidgetInfo = [
];
export const getClusterMetricsQueryPayload = (
cluster: InframonitoringtypesClusterRecordDTO,
cluster: K8sClusterData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -193,7 +207,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -232,7 +246,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -271,7 +285,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -310,7 +324,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -383,7 +397,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -422,7 +436,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -461,7 +475,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -500,7 +514,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -573,7 +587,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -659,7 +673,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -745,7 +759,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -797,7 +811,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -895,7 +909,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -947,7 +961,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -999,7 +1013,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1051,7 +1065,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1173,7 +1187,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1219,7 +1233,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1265,7 +1279,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1369,7 +1383,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1421,7 +1435,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1473,7 +1487,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1525,7 +1539,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',

View File

@@ -1,48 +1,42 @@
import { Color } from '@signozhq/design-tokens';
import { Boxes } from '@signozhq/icons';
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sClusterData, K8sClustersListPayload } from './api';
import { Boxes } from '@signozhq/icons';
export function getK8sClusterRowKey(
cluster: InframonitoringtypesClusterRecordDTO,
): string {
export function getK8sClusterRowKey(cluster: K8sClusterData): string {
return (
cluster.clusterName ||
cluster.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_UID] ||
''
cluster.clusterUID ||
cluster.meta.k8s_cluster_uid ||
cluster.meta.k8s_cluster_name
);
}
export function getK8sClusterItemKey(
cluster: InframonitoringtypesClusterRecordDTO,
): string {
return cluster.clusterName;
export function getK8sClusterItemKey(cluster: K8sClusterData): string {
return cluster.meta.k8s_cluster_name;
}
export type ClusterTableColumnConfig =
TableColumnDef<InframonitoringtypesClusterRecordDTO>;
export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
{
id: 'clusterGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Cluster Group" />,
accessorFn: (row): string => row.clusterName || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="CLUSTER GROUP" />,
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -65,10 +59,9 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
<EntityGroupHeader
title="Cluster Name"
icon={<Boxes data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/clusters#cluster-name"
/>
),
accessorFn: (row): string => row.clusterName || '',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -78,127 +71,55 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const clusterName = value as string;
return (
<CellValueTooltip value={clusterName}>
<Tooltip title={clusterName}>
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
id: 'nodeCountsByReadiness',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#node-readiness">
Node Readiness
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesClusterRecordDTO['nodeCountsByReadiness'] =>
row.nodeCountsByReadiness,
width: { min: 180 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
if (!row.nodeCountsByReadiness) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
items={[
{
value: row.nodeCountsByReadiness.ready,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.nodeCountsByReadiness.notReady,
label: 'Not Ready',
color: Color.BG_AMBER_500,
},
]}
/>
);
},
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesClusterRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
</Tooltip>
);
},
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterCPU,
width: { min: 160 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-alloc-cores">
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterCPUAllocatable,
width: { min: 200 },
header: 'CPU Alloc (cores)',
accessorFn: (row): number => row.cpuAllocatable,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = Number(value);
const cpuAllocatable = value as number;
return (
<ValidateColumnValueWrapper
value={cpuAllocatable}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU allocatable metric"
>
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemory,
width: { min: 180 },
header: 'Memory Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -215,14 +136,9 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
},
{
id: 'memory_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-allocatable">
Memory
<br /> Allocatable
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemoryAllocatable,
width: { min: 180 },
header: 'Memory Allocatable',
accessorFn: (row): number => row.memoryAllocatable,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;

View File

@@ -1,117 +1,93 @@
import { useCallback } from 'react';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sDaemonSetsList, K8sDaemonSetsData } from './api';
import {
daemonSetWidgetInfo,
getDaemonSetMetricsQueryPayload,
k8sDaemonSetDetailsMetadataConfig,
k8sDaemonSetGetEntityName,
k8sDaemonSetGetSelectedItemExpression,
k8sDaemonSetInitialEventsExpression,
k8sDaemonSetInitialLogTracesExpression,
k8sDaemonSetGetSelectedItemFilters,
k8sDaemonSetInitialEventsFilter,
k8sDaemonSetInitialLogTracesFilter,
} from './constants';
import {
getK8sDaemonSetItemKey,
getK8sDaemonSetRowKey,
k8sDaemonSetsColumnsConfig,
} from './table.config';
function K8sDaemonSetsList({
controlListPrefix,
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listDaemonSets(
{
filter: { expression: filters.filter.expression },
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch daemonsets';
return {
type: 'list' as const,
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
total: 0,
error: errMsg,
};
}
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
const response = await getK8sDaemonSetsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDaemonSetRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listDaemonSets(
{
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 daemonset';
return {
data: null,
error: errMsg,
};
}
): Promise<{ data: K8sDaemonSetsData | null; error?: string | null }> => {
const response = await getK8sDaemonSetsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
<K8sBaseList<K8sDaemonSetsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}
@@ -120,14 +96,15 @@ function K8sDaemonSetsList({
getItemKey={getK8sDaemonSetItemKey}
eventCategory={InfraMonitoringEvents.DaemonSet}
/>
<K8sBaseDetails<InframonitoringtypesDaemonSetRecordDTO>
<K8sBaseDetails<K8sDaemonSetsData>
category={InfraMonitoringEntity.DAEMONSETS}
eventCategory={InfraMonitoringEvents.DaemonSet}
getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression}
getSelectedItemFilters={k8sDaemonSetGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sDaemonSetGetEntityName}
getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression}
getInitialEventsExpression={k8sDaemonSetInitialEventsExpression}
getInitialLogTracesFilters={k8sDaemonSetInitialLogTracesFilter}
getInitialEventsFilters={k8sDaemonSetInitialEventsFilter}
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
entityWidgetInfo={daemonSetWidgetInfo}
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
@@ -136,4 +113,5 @@ function K8sDaemonSetsList({
</>
);
}
export default K8sDaemonSetsList;

View File

@@ -0,0 +1,119 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
export interface K8sDaemonSetsData {
daemonSetName: string;
cpuUsage: number;
memoryUsage: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
desiredNodes: number;
availableNodes: number;
meta: {
k8s_cluster_name: string;
k8s_daemonset_name: string;
k8s_namespace_name: string;
};
}
export interface K8sDaemonSetsListResponse {
status: string;
data: {
type: string;
records: K8sDaemonSetsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const daemonSetsMetaMap = [
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapDaemonSetsMeta(
raw: Record<string, unknown>,
): K8sDaemonSetsData['meta'] {
const out: Record<string, unknown> = { ...raw };
daemonSetsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sDaemonSetsData['meta'];
}
export const getK8sDaemonSetsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sDaemonSetsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/daemonsets/list', requestProps, {
signal,
headers,
});
const payload: K8sDaemonSetsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapDaemonSetsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,52 +1,67 @@
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sDaemonSetsData } from './api';
export const k8sDaemonSetGetSelectedItemExpression = (
export const k8sDaemonSetGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_daemonset_name',
key: {
key: 'k8s_daemonset_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDaemonSetRecordDTO>[] =
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDaemonSetsData>[] =
[
{
label: 'Daemonset Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
getValue: (p): string => p.meta.k8s_daemonset_name,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_name,
},
];
export const k8sDaemonSetInitialEventsExpression = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'DaemonSet' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')}`;
export const k8sDaemonSetInitialEventsFilter = (
item: K8sDaemonSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'DaemonSet'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_daemonset_name),
];
export const k8sDaemonSetInitialLogTracesExpression = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '')}`;
export const k8sDaemonSetInitialLogTracesFilter = (
item: K8sDaemonSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_DAEMON_SET_NAME, item.meta.k8s_daemonset_name),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
];
export const k8sDaemonSetGetEntityName = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '';
export const k8sDaemonSetGetEntityName = (item: K8sDaemonSetsData): string =>
item.meta.k8s_daemonset_name;
export const daemonSetWidgetInfo = [
{
@@ -68,7 +83,7 @@ export const daemonSetWidgetInfo = [
];
export const getDaemonSetMetricsQueryPayload = (
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
daemonSet: K8sDaemonSetsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -144,9 +159,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -157,9 +170,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -198,9 +209,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -211,9 +220,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -252,9 +259,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -265,9 +270,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -340,9 +343,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -353,9 +354,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -394,9 +393,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -407,9 +404,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -448,9 +443,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -461,9 +454,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -536,9 +527,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -549,9 +538,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -637,9 +624,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -650,9 +635,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',

View File

@@ -1,49 +1,34 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sDaemonSetsData } from './api';
import { Group } from '@signozhq/icons';
export function getK8sDaemonSetRowKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
export function getK8sDaemonSetRowKey(daemonSet: K8sDaemonSetsData): string {
return (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ||
daemonSet.daemonSetName ||
`${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME]}-${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME]}`
daemonSet.meta.k8s_daemonset_name ||
`${daemonSet.meta.k8s_namespace_name}-${daemonSet.meta.k8s_daemonset_name}`
);
}
export function getK8sDaemonSetItemKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
return daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '';
export function getK8sDaemonSetItemKey(daemonSet: K8sDaemonSetsData): string {
return daemonSet.meta.k8s_daemonset_name;
}
export type DaemonSetTableColumnConfig =
TableColumnDef<InframonitoringtypesDaemonSetRecordDTO>;
export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
{
id: 'daemonSetGroup',
header: (): React.ReactNode => <EntityGroupHeader title="DaemonSet Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="DAEMONSET GROUP" />,
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -66,11 +51,9 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
<EntityGroupHeader
title="DaemonSet Name"
icon={<Group data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/daemonsets#daemonset-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -80,108 +63,71 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const daemonsetName = value as string;
return (
<CellValueTooltip value={daemonsetName}>
<Tooltip title={daemonsetName}>
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 160 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 100 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesDaemonSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
id: 'available_nodes',
header: 'Available',
accessorFn: (row): number => row.availableNodes,
width: { min: 140 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const availableNodes = value as number;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
<ValidateColumnValueWrapper
value={availableNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="available node"
>
<TanStackTable.Text>{availableNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'node_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#node-status">
Node Status
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,
width: { min: 210 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.readyNodes,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.currentNodes,
label: 'Current',
color: Color.BG_ROBIN_500,
},
{
value: row.desiredNodes,
label: 'Desired',
color: Color.BG_SAKURA_400,
},
{
value: row.misscheduledNodes,
label: 'Misscheduled',
color: Color.BG_AMBER_500,
},
]}
/>
),
id: 'desired_nodes',
header: 'Desired',
accessorFn: (row): number => row.desiredNodes,
width: { min: 140 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const desiredNodes = value as number;
return (
<ValidateColumnValueWrapper
value={desiredNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="desired node"
>
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPURequest,
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -197,16 +143,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPULimit,
width: { min: 160 },
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
@@ -222,42 +162,30 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPU,
width: { min: 160 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryRequest,
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
@@ -274,16 +202,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryLimit,
width: { min: 190 },
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
@@ -299,16 +221,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemory,
width: { min: 180 },
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
@@ -322,100 +238,4 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
id: 'ready_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#ready">
Ready Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.readyNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const readyNodes = value as number;
return (
<ValidateColumnValueWrapper
value={readyNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="ready node"
>
<TanStackTable.Text>{readyNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'current_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#current">
Current Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const currentNodes = value as number;
return (
<ValidateColumnValueWrapper
value={currentNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="current node"
>
<TanStackTable.Text>{currentNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#desired">
Desired Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.desiredNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredNodes = value as number;
return (
<ValidateColumnValueWrapper
value={desiredNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="desired node"
>
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'misscheduled_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#misscheduled">
Misscheduled Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.misscheduledNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const misscheduledNodes = value as number;
return (
<ValidateColumnValueWrapper
value={misscheduledNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="misscheduled node"
>
<TanStackTable.Text>{misscheduledNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -1,24 +1,21 @@
import { useCallback } from 'react';
import { listDeployments } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sDeploymentsList, K8sDeploymentsData } from './api';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
k8sDeploymentDetailsMetadataConfig,
k8sDeploymentGetEntityName,
k8sDeploymentGetSelectedItemExpression,
k8sDeploymentInitialEventsExpression,
k8sDeploymentInitialLogTracesExpression,
k8sDeploymentGetSelectedItemFilters,
k8sDeploymentInitialEventsFilter,
k8sDeploymentInitialLogTracesFilter,
} from './constants';
import {
getK8sDeploymentItemKey,
@@ -31,93 +28,66 @@ function K8sDeploymentsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch deployments';
return {
type: 'list' as const,
records: [] as InframonitoringtypesDeploymentRecordDTO[],
total: 0,
error: errMsg,
};
}
const response = await getK8sDeploymentsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDeploymentRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listDeployments(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
): Promise<{ data: K8sDeploymentsData | null; error?: string | null }> => {
const response = await getK8sDeploymentsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch deployment';
return {
data: null,
error: errMsg,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
<K8sBaseList<K8sDeploymentsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}
@@ -127,14 +97,14 @@ function K8sDeploymentsList({
eventCategory={InfraMonitoringEvents.Deployment}
/>
<K8sBaseDetails<InframonitoringtypesDeploymentRecordDTO>
<K8sBaseDetails<K8sDeploymentsData>
category={InfraMonitoringEntity.DEPLOYMENTS}
eventCategory={InfraMonitoringEvents.Deployment}
getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression}
getSelectedItemFilters={k8sDeploymentGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sDeploymentGetEntityName}
getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression}
getInitialEventsExpression={k8sDeploymentInitialEventsExpression}
getInitialLogTracesFilters={k8sDeploymentInitialLogTracesFilter}
getInitialEventsFilters={k8sDeploymentInitialEventsFilter}
metadataConfig={k8sDeploymentDetailsMetadataConfig}
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}

View File

@@ -0,0 +1,132 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sDeploymentsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sDeploymentsData {
deploymentName: string;
cpuUsage: number;
memoryUsage: number;
desiredPods: number;
availablePods: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
meta: {
k8s_cluster_name: string;
k8s_deployment_name: string;
k8s_namespace_name: string;
};
}
export interface K8sDeploymentsListResponse {
status: string;
data: {
type: string;
records: K8sDeploymentsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const deploymentsMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapDeploymentsMeta(
raw: Record<string, unknown>,
): K8sDeploymentsData['meta'] {
const out: Record<string, unknown> = { ...raw };
deploymentsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sDeploymentsData['meta'];
}
export const getK8sDeploymentsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/deployments/list', requestProps, {
signal,
headers,
});
const payload: K8sDeploymentsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapDeploymentsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,63 +1,70 @@
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sDeploymentsData } from './api';
export const k8sDeploymentGetSelectedItemExpression = (
export const k8sDeploymentGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_deployment_name',
key: {
key: 'k8s_deployment_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDeploymentRecordDTO>[] =
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDeploymentsData>[] =
[
{
label: 'Deployment Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
getValue: (p): string => p.meta.k8s_deployment_name,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_name,
},
];
export const k8sDeploymentInitialEventsExpression = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => {
const objectName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Deployment' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
};
export const k8sDeploymentInitialEventsFilter = (
item: K8sDeploymentsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Deployment'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_deployment_name),
];
export const k8sDeploymentInitialLogTracesExpression = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => {
const deploymentName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${deploymentName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
export const k8sDeploymentInitialLogTracesFilter = (
item: K8sDeploymentsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
item.meta.k8s_deployment_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
];
export const k8sDeploymentGetEntityName = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '';
export const k8sDeploymentGetEntityName = (item: K8sDeploymentsData): string =>
item.meta.k8s_deployment_name;
export const deploymentWidgetInfo = [
{
@@ -79,7 +86,7 @@ export const deploymentWidgetInfo = [
];
export const getDeploymentMetricsQueryPayload = (
deployment: InframonitoringtypesDeploymentRecordDTO,
deployment: K8sDeploymentsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -151,9 +158,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -192,9 +197,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -233,9 +236,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -308,9 +309,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -349,9 +348,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -390,9 +387,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -465,9 +460,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -553,9 +546,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',

View File

@@ -1,50 +1,35 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sDeploymentsData } from './api';
import { Computer } from '@signozhq/icons';
export function getK8sDeploymentRowKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
): string {
return (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ||
deployment.deploymentName ||
''
);
export function getK8sDeploymentRowKey(deployment: K8sDeploymentsData): string {
return deployment.meta.k8s_deployment_name || deployment.deploymentName;
}
export function getK8sDeploymentItemKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
deployment: K8sDeploymentsData,
): string {
return deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '';
return deployment.meta.k8s_deployment_name;
}
export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDeploymentRecordDTO>[] =
export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
[
{
id: 'deploymentGroup',
header: (): React.ReactNode => (
<EntityGroupHeader title="Deployment Group" />
<EntityGroupHeader title="DEPLOYMENT GROUP" />
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
width: { min: 220 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -67,12 +52,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
<EntityGroupHeader
title="Deployment Name"
icon={<Computer data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/deployments#deployment-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
width: { min: 210 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -81,239 +64,30 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
cell: ({ value }): React.ReactNode => {
const deploymentName = value as string;
return (
<CellValueTooltip value={deploymentName}>
<Tooltip title={deploymentName}>
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 160 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 220 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value as string}</TanStackTable.Text>
),
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): object | undefined => row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'replica_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#replica-status">
Replica Status
</ColumnHeader>
),
accessorFn: (row): number => row.availablePods,
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.availablePods,
label: 'Available',
color: Color.BG_FOREST_500,
},
{
value: row.desiredPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
},
]}
/>
),
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPURequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
value={cpuRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPULimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = Number(value);
return (
<ValidateColumnValueWrapper
value={cpuLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemoryRequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = Number(value);
return (
<ValidateColumnValueWrapper
value={memoryRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemoryLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = Number(value);
return (
<ValidateColumnValueWrapper
value={memoryLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemory,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="memory metric"
>
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'available_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#available">
Available Pods
</ColumnHeader>
),
header: 'Available',
accessorFn: (row): number => row.availablePods,
width: { min: 120 },
enableSort: true,
width: { min: 100 },
enableSort: false,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const availablePods = value as number;
@@ -330,14 +104,11 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
},
{
id: 'desired_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#desired">
Desired Pods
</ColumnHeader>
),
header: 'Desired',
accessorFn: (row): number => row.desiredPods,
width: { min: 120 },
enableSort: true,
width: { min: 80 },
enableSort: false,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
@@ -352,4 +123,124 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
);
},
},
{
id: 'cpu_request',
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
value={cpuRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Request"
>
<EntityProgressBar value={cpuRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_limit',
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
<ValidateColumnValueWrapper
value={cpuLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Limit"
>
<EntityProgressBar value={cpuLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu',
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
<ValidateColumnValueWrapper
value={memoryRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Request"
>
<EntityProgressBar value={memoryRequest} type="request" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_limit',
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
<ValidateColumnValueWrapper
value={memoryLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Limit"
>
<EntityProgressBar value={memoryLimit} type="limit" />
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 140 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="memory metric"
>
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -14,12 +14,7 @@ import {
import APIError from 'types/api/error';
import { v4 as uuidv4 } from 'uuid';
import { INFRA_MONITORING_ATTR_KEYS } from '../../constants';
const K8S_EVENT_KEYS = [
INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND,
INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME,
];
const K8S_EVENT_KEYS = ['k8s.object.kind', 'k8s.object.name'];
export function isEventsKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {

View File

@@ -14,28 +14,11 @@
box-sizing: border-box;
}
.entityMetricsTitleContainer {
display: flex;
align-items: center;
gap: 8px;
}
.entityMetricsTitle {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}
.metricsExplorerLink {
display: flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.metricsHeader {
display: flex;
justify-content: flex-end;

View File

@@ -1,8 +1,6 @@
import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Link } from 'react-router-dom';
import { Compass } from '@signozhq/icons';
import { Skeleton, Tooltip } from 'antd';
import { Skeleton } from 'antd';
import cx from 'classnames';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
@@ -21,7 +19,6 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getMetricsExplorerUrl } from 'utils/explorerUtils';
import { buildEntityMetricsChartConfig } from './configBuilder';
@@ -207,31 +204,9 @@ function EntityMetrics<T>({
key={entityWidgetInfo[idx].title}
className={styles.entityMetricsCol}
>
<div className={styles.entityMetricsTitleContainer}>
<span className={styles.entityMetricsTitle}>
{entityWidgetInfo[idx].title}
</span>
{queryPayloads[idx] &&
queryPayloads[idx].graphType !== PANEL_TYPES.TABLE && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})}
className={styles.metricsExplorerLink}
data-testid={`open-metrics-explorer-${idx}`}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
<span className={styles.entityMetricsTitle}>
{entityWidgetInfo[idx].title}
</span>
<div className={styles.entityMetricsCard} ref={graphRef}>
{renderCardContent(query, idx)}
</div>

View File

@@ -1,5 +1,4 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import * as appContextHooks from 'providers/App/App';
@@ -296,19 +295,17 @@ const renderEntityMetrics = (overrides = {}): any => {
};
return render(
<MemoryRouter>
<EntityMetrics
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
entity={defaultProps.entity}
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>
</MemoryRouter>,
<EntityMetrics
timeRange={defaultProps.timeRange}
isModalTimeSelection={defaultProps.isModalTimeSelection}
handleTimeChange={defaultProps.handleTimeChange}
selectedInterval={defaultProps.selectedInterval}
entity={defaultProps.entity}
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>,
);
};
@@ -337,8 +334,8 @@ const mockTableData: (import('../utils').MetricsTableData[] | null)[] = [
];
const mockQueryPayloads = [
{ graphType: 'graph', query: { queryType: 'builder' } }, // time_series
{ graphType: 'table', query: { queryType: 'builder' } }, // table
{ graphType: 'graph' }, // time_series
{ graphType: 'table' }, // table
];
describe('EntityMetrics', () => {
@@ -445,34 +442,6 @@ describe('EntityMetrics', () => {
);
});
it('renders metrics explorer link only for non-table panels', () => {
renderEntityMetrics();
expect(screen.getByTestId('open-metrics-explorer-0')).toBeInTheDocument();
expect(
screen.queryByTestId('open-metrics-explorer-1'),
).not.toBeInTheDocument();
});
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
renderEntityMetrics({ selectedInterval: '5m' as Time });
const href = screen
.getByTestId('open-metrics-explorer-0')
.getAttribute('href');
expect(href).toContain('relativeTime=5m');
expect(href).not.toContain('startTime=');
expect(href).not.toContain('endTime=');
});
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
renderEntityMetrics({ selectedInterval: 'custom' as Time });
const href = screen
.getByTestId('open-metrics-explorer-0')
.getAttribute('href');
expect(href).toContain(`startTime=${mockTimeRange.startTime * 1000}`);
expect(href).toContain(`endTime=${mockTimeRange.endTime * 1000}`);
expect(href).not.toContain('relativeTime=');
});
it('passes correct parameters to useEntityMetrics hook', () => {
renderEntityMetrics();
expect(mockUseEntityMetrics).toHaveBeenCalledWith(

View File

@@ -1,8 +1,144 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import APIError from 'types/api/error';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { nanoToMilli } from 'utils/timeUtils';
import { v4 as uuidv4 } from 'uuid';
export function isKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {
return false;
}
const errorDetails = error.getErrorDetails();
if (errorDetails.error.code !== 'invalid_input') {
return false;
}
const errors = errorDetails.error.errors || [];
return errors.some((err) => err.message?.includes('not found'));
}
export const QUERY_KEYS = {
K8S_OBJECT_KIND: 'k8s.object.kind',
K8S_OBJECT_NAME: 'k8s.object.name',
K8S_POD_NAME: 'k8s.pod.name',
K8S_NAMESPACE_NAME: 'k8s.namespace.name',
K8S_CLUSTER_NAME: 'k8s.cluster.name',
K8S_NODE_NAME: 'k8s.node.name',
K8S_DEPLOYMENT_NAME: 'k8s.deployment.name',
K8S_STATEFUL_SET_NAME: 'k8s.statefulset.name',
K8S_JOB_NAME: 'k8s.job.name',
K8S_DAEMON_SET_NAME: 'k8s.daemonset.name',
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
};
/**
* Returns the payload configuration to fetch events for a K8s entity
*/
export const getEntityEventsOrLogsQueryPayload = (
start: number,
end: number,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
query: {
clickhouse_sql: [],
promql: [],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.String,
key: '',
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: ReduceOperators.AVG,
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,
},
start,
end,
});
export const entityTracesColumns = [
{
dataIndex: 'timestamp',
key: 'timestamp',
title: 'Timestamp',
width: 200,
render: (timestamp: string): string => new Date(timestamp).toLocaleString(),
},
{
title: 'Service Name',
dataIndex: ['data', 'serviceName'],
key: 'serviceName-string-tag',
width: 150,
},
{
title: 'Name',
dataIndex: ['data', 'name'],
key: 'name-string-tag',
width: 145,
},
{
title: 'Duration',
dataIndex: ['data', 'durationNano'],
key: 'durationNano-float64-tag',
width: 145,
render: (duration: number): string => `${nanoToMilli(duration)}ms`,
},
{
title: 'HTTP Method',
dataIndex: ['data', 'httpMethod'],
key: 'httpMethod-string-tag',
width: 145,
},
{
title: 'Status Code',
dataIndex: ['data', 'responseStatusCode'],
key: 'responseStatusCode-string-tag',
width: 145,
},
];
export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
{
@@ -37,16 +173,110 @@ export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
},
];
export function isKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {
return false;
}
export const getEntityTracesQueryPayload = (
start: number,
end: number,
offset = 0,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
query: {
promql: [],
clickhouse_sql: [],
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.EMPTY,
key: '',
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: ReduceOperators.AVG,
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
params: {
dataSource: DataSource.TRACES,
},
tableParams: {
pagination: {
limit: 10,
offset,
},
selectColumns: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
dataType: 'string',
type: 'tag',
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
],
},
});
const errorDetails = error.getErrorDetails();
if (errorDetails.error.code !== 'invalid_input') {
return false;
}
const errors = errorDetails.error.errors || [];
return errors.some((err) => err.message?.includes('not found'));
}
export const filterOutPrimaryFilters = (
filters: TagFilterItem[],
primaryKeys: string[],
): TagFilterItem[] =>
filters.filter(
(filter) =>
!primaryKeys.includes(filter.key?.key ?? '') && filter.key?.key !== 'id',
);

View File

@@ -26,8 +26,8 @@
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
@@ -35,12 +35,11 @@
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
@@ -85,7 +84,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
@@ -100,12 +99,11 @@
.listContainer {
flex: 1;
min-width: 0;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 595px;
overflow-x: auto;
> * {
min-width: 0;
@@ -235,7 +233,3 @@
background: var(--accent-primary);
}
}
.k8SOpenQuickFilters {
grid-area: quickFilters;
}

View File

@@ -2,11 +2,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { initialQueriesMap } from 'constants/queryBuilder';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { InfraMonitoringEvents } from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
ArrowUpDown,
ArrowUpToLine,
@@ -21,7 +22,7 @@ import {
Workflow,
} from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
@@ -42,9 +43,9 @@ import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
import {
useInfraMonitoringCategory,
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItem,
} from './hooks';
import K8sJobsList from './Jobs/K8sJobsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
@@ -54,61 +55,63 @@ import K8sStatefulSetsList from './StatefulSets/K8sStatefulSetsList';
import K8sVolumesList from './Volumes/K8sVolumesList';
import styles from './InfraMonitoringK8s.module.scss';
import { InfraMonitoringEvents } from 'constants/events';
import logEvent from 'api/common/logEvent';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setOrderBy] = useInfraMonitoringOrderBy();
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const compositeQuery = useGetCompositeQueryParam();
const { currentQuery, 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]);
useEffect(() => {
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: selectedCategory,
view: InfraMonitoringEvents.QuickFiltersView,
});
}, [selectedCategory]);
const { currentQuery } = useQueryBuilder();
const handleFilterVisibilityChange = useCallback((): void => {
setShowFilters((show) => !show);
}, []);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
// Track previous urlFilters to only sync when the value actually changes
// (not when handleChangeQueryData changes due to query updates)
const prevUrlFiltersRef = useRef<string | null>(null);
useEffect(() => {
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
// Only sync if urlFilters value has actually changed
if (prevUrlFiltersRef.current !== currentFiltersJson) {
prevUrlFiltersRef.current = currentFiltersJson;
// Sync filters to query builder, using empty filter when urlFilters is null
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const handleFilterChange = (query: Query): void => {
// update the current query with the new filters
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
const filters = query.builder.queryData[0].filters;
// The useEffect will sync filters to query builder, avoiding double state updates
setUrlFilters(filters || null);
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: selectedCategory,
view: InfraMonitoringEvents.QuickFiltersView,
});
};
const categories = useMemo(
() => [
{
@@ -176,23 +179,12 @@ export default function InfraMonitoringK8s(): JSX.Element {
const handleCategorySelect = (key: string): void => {
if (key !== selectedCategory) {
void setSelectedCategory(key as string);
void setOrderBy(null);
void setGroupBy(null);
void setSelectedItem(null);
redirectWithQueryBuilderData({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...(currentQuery.builder.queryData[0] || {}),
filter: { expression: '' },
filters: { items: [], op: 'AND' as const },
},
],
},
});
setSelectedCategory(key);
// Reset filters
setUrlFilters(null);
setOrderBy(null);
setGroupBy(null);
handleChangeQueryData('filters', { items: [], op: 'and' });
}
};
@@ -200,7 +192,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
return (
<>
{!showFilters && (
<div className={styles.k8SOpenQuickFilters}>
<div>
<Button
className="periscope-btn ghost"
type="text"
@@ -269,6 +261,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
)}
</div>

View File

@@ -1,24 +1,21 @@
import { useCallback } from 'react';
import { listJobs } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sJobsList, K8sJobsData } from './api';
import {
getJobMetricsQueryPayload,
jobWidgetInfo,
k8sJobDetailsMetadataConfig,
k8sJobGetEntityName,
k8sJobGetSelectedItemExpression,
k8sJobInitialEventsExpression,
k8sJobInitialLogTracesExpression,
k8sJobGetSelectedItemFilters,
k8sJobInitialEventsFilter,
k8sJobInitialLogTracesFilter,
} from './constants';
import {
getK8sJobItemKey,
@@ -31,93 +28,66 @@ function K8sJobsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch jobs';
return {
type: 'list' as const,
records: [] as InframonitoringtypesJobRecordDTO[],
total: 0,
error: errMsg,
};
}
const response = await getK8sJobsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesJobRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listJobs(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
): Promise<{ data: K8sJobsData | null; error?: string | null }> => {
const response = await getK8sJobsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch job';
return {
data: null,
error: errMsg,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesJobRecordDTO>
<K8sBaseList<K8sJobsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}
@@ -127,14 +97,14 @@ function K8sJobsList({
eventCategory={InfraMonitoringEvents.Job}
/>
<K8sBaseDetails<InframonitoringtypesJobRecordDTO>
<K8sBaseDetails<K8sJobsData>
category={InfraMonitoringEntity.JOBS}
eventCategory={InfraMonitoringEvents.Job}
getSelectedItemExpression={k8sJobGetSelectedItemExpression}
getSelectedItemFilters={k8sJobGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sJobGetEntityName}
getInitialLogTracesExpression={k8sJobInitialLogTracesExpression}
getInitialEventsExpression={k8sJobInitialEventsExpression}
getInitialLogTracesFilters={k8sJobInitialLogTracesFilter}
getInitialEventsFilters={k8sJobInitialEventsFilter}
metadataConfig={k8sJobDetailsMetadataConfig}
entityWidgetInfo={jobWidgetInfo}
getEntityQueryPayload={getJobMetricsQueryPayload}

View File

@@ -0,0 +1,119 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
export interface K8sJobsData {
jobName: string;
cpuUsage: number;
memoryUsage: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
desiredSuccessfulPods: number;
activePods: number;
failedPods: number;
successfulPods: number;
meta: {
k8s_cluster_name: string;
k8s_job_name: string;
k8s_namespace_name: string;
};
}
export interface K8sJobsListResponse {
status: string;
data: {
type: string;
records: K8sJobsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const jobsMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapJobsMeta(raw: Record<string, unknown>): K8sJobsData['meta'] {
const out: Record<string, unknown> = { ...raw };
jobsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sJobsData['meta'];
}
export const getK8sJobsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sJobsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters?.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/jobs/list', requestProps, {
signal,
headers,
});
const payload: K8sJobsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapJobsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,63 +1,67 @@
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sJobsData } from './api';
export const k8sJobGetSelectedItemExpression = (
export const k8sJobGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_job_name',
key: {
key: 'k8s_job_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesJobRecordDTO>[] =
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sJobsData>[] =
[
{
label: 'Job Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
getValue: (p): string => p.meta.k8s_job_name,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_name,
},
];
export const k8sJobInitialEventsExpression = (
item: InframonitoringtypesJobRecordDTO,
): string => {
const name = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Job' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${name}`;
};
export const k8sJobInitialEventsFilter = (
item: K8sJobsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Job'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_job_name),
];
export const k8sJobInitialLogTracesExpression = (
item: InframonitoringtypesJobRecordDTO,
): string => {
const jobName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${jobName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
export const k8sJobInitialLogTracesFilter = (
item: K8sJobsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_JOB_NAME, item.meta.k8s_job_name),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
];
export const k8sJobGetEntityName = (
item: InframonitoringtypesJobRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '';
export const k8sJobGetEntityName = (item: K8sJobsData): string =>
item.meta.k8s_job_name;
export const jobWidgetInfo = [
{
@@ -79,7 +83,7 @@ export const jobWidgetInfo = [
];
export const getJobMetricsQueryPayload = (
job: InframonitoringtypesJobRecordDTO,
job: K8sJobsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -141,8 +145,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -226,8 +229,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -311,8 +313,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -409,8 +410,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',

View File

@@ -1,45 +1,30 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sJobsData } from './api';
import { Bolt } from '@signozhq/icons';
export function getK8sJobRowKey(job: InframonitoringtypesJobRecordDTO): string {
return (
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || job.jobName || ''
);
export function getK8sJobRowKey(job: K8sJobsData): string {
return job.jobName || job.meta.k8s_job_name || '';
}
export function getK8sJobItemKey(
job: InframonitoringtypesJobRecordDTO,
): string {
return job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '';
export function getK8sJobItemKey(job: K8sJobsData): string {
return job.meta.k8s_job_name;
}
export type JobTableColumnConfig =
TableColumnDef<InframonitoringtypesJobRecordDTO>;
export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
{
id: 'jobGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Job Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="JOB GROUP" />,
accessorFn: (row): string => row.meta.k8s_job_name || '',
width: { min: 270 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -62,12 +47,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
<EntityGroupHeader
title="Job Name"
icon={<Bolt data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/jobs#job-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_job_name || '',
width: { min: 260 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -76,98 +59,109 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const jobName = value as string;
return (
<CellValueTooltip value={jobName}>
<Tooltip title={jobName}>
<TanStackTable.Text>{jobName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#namespace-name">
Namespace
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 160 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 150 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'completion_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion-status">
Completion Status
</ColumnHeader>
),
id: 'successful_pods',
header: 'Successful',
accessorFn: (row): number => row.successfulPods,
width: { min: 210 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{ value: row.activePods, label: 'Active', color: Color.BG_ROBIN_500 },
{ value: row.failedPods, label: 'Failed', color: Color.BG_CHERRY_500 },
{
value: row.successfulPods,
label: 'Successful',
color: Color.BG_FOREST_500,
},
{
value: row.desiredSuccessfulPods,
label: 'Desired',
color: Color.BG_ROBIN_500,
},
]}
/>
),
width: { min: 120 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const successfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={successfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="successful pod"
>
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'failed_pods',
header: 'Failed',
accessorFn: (row): number => row.failedPods,
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const failedPods = value as number;
return (
<ValidateColumnValueWrapper
value={failedPods}
entity={InfraMonitoringEntity.JOBS}
attribute="failed pod"
>
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_successful_pods',
header: 'Desired Successful',
accessorFn: (row): number => row.desiredSuccessfulPods,
width: { min: 160 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const desiredSuccessfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredSuccessfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="desired successful pod"
>
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'active_pods',
header: 'Active',
accessorFn: (row): number => row.activePods,
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const activePods = value as number;
return (
<ValidateColumnValueWrapper
value={activePods}
entity={InfraMonitoringEntity.JOBS}
attribute="active pod"
>
<TanStackTable.Text>{activePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPURequest,
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -183,16 +177,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPULimit,
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
@@ -208,42 +196,29 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPU,
width: { min: 160 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.JOBS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryRequest,
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -259,16 +234,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryLimit,
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
@@ -284,16 +253,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemory,
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
@@ -307,100 +270,4 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
);
},
},
{
id: 'active_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#active">
Active Pods
</ColumnHeader>
),
accessorFn: (row): number => row.activePods,
width: { min: 100 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const activePods = value as number;
return (
<ValidateColumnValueWrapper
value={activePods}
entity={InfraMonitoringEntity.JOBS}
attribute="active pod"
>
<TanStackTable.Text>{activePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'failed_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#failed">
Failed Pods
</ColumnHeader>
),
accessorFn: (row): number => row.failedPods,
width: { min: 100 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const failedPods = value as number;
return (
<ValidateColumnValueWrapper
value={failedPods}
entity={InfraMonitoringEntity.JOBS}
attribute="failed pod"
>
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'successful_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#successful">
Successful Pods
</ColumnHeader>
),
accessorFn: (row): number => row.successfulPods,
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const successfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={successfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="successful pod"
>
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'desired_successful_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#desired-successful">
Desired Successful Pods
</ColumnHeader>
),
accessorFn: (row): number => row.desiredSuccessfulPods,
width: { min: 160 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredSuccessfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredSuccessfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="desired successful pod"
>
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sNamespacesList, K8sNamespacesData } from './api';
import {
getNamespaceMetricsQueryPayload,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
k8sNamespaceInitialLogTracesExpression,
k8sNamespaceGetSelectedItemFilters,
k8sNamespaceInitialEventsFilter,
k8sNamespaceInitialLogTracesFilter,
namespaceWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ function K8sNamespacesList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch namespaces';
return {
type: 'list' as const,
records: [] as InframonitoringtypesNamespaceRecordDTO[],
total: 0,
error: errMsg,
};
}
const response = await getK8sNamespacesList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNamespaceRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listNamespaces(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
): Promise<{ data: K8sNamespacesData | null; error?: string | null }> => {
const response = await getK8sNamespacesList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch namespace';
return {
data: null,
error: errMsg,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
<K8sBaseList<K8sNamespacesData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
@@ -127,14 +97,14 @@ function K8sNamespacesList({
eventCategory={InfraMonitoringEvents.Namespace}
/>
<K8sBaseDetails<InframonitoringtypesNamespaceRecordDTO>
<K8sBaseDetails<K8sNamespacesData>
category={InfraMonitoringEntity.NAMESPACES}
eventCategory={InfraMonitoringEvents.Namespace}
getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression}
getSelectedItemFilters={k8sNamespaceGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sNamespaceGetEntityName}
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
getInitialLogTracesFilters={k8sNamespaceInitialLogTracesFilter}
getInitialEventsFilters={k8sNamespaceInitialEventsFilter}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}

View File

@@ -0,0 +1,123 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sNamespacesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNamespacesData {
namespaceName: string;
cpuUsage: number;
memoryUsage: number;
meta: {
k8s_cluster_name: string;
k8s_namespace_name: string;
};
}
export interface K8sNamespacesListResponse {
status: string;
data: {
type: string;
records: K8sNamespacesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const namespacesMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapNamespacesMeta(
raw: Record<string, unknown>,
): K8sNamespacesData['meta'] {
const out: Record<string, unknown> = { ...raw };
namespacesMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sNamespacesData['meta'];
}
export const getK8sNamespacesList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sNamespacesListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters?.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/namespaces/list', requestProps, {
signal,
headers,
});
const payload: K8sNamespacesListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapNamespacesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,47 +1,67 @@
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sNamespacesData } from './api';
export const k8sNamespaceGetSelectedItemExpression = (
export const k8sNamespaceGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_namespace_name',
key: {
key: 'k8s_namespace_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNamespaceRecordDTO>[] =
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNamespacesData>[] =
[
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName || '' },
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName },
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
];
export const k8sNamespaceInitialEventsExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Namespace' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${name}`;
};
export const k8sNamespaceInitialFilters = [
QUERY_KEYS.K8S_NAMESPACE_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
];
export const k8sNamespaceInitialLogTracesExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${name}`;
};
export const k8sNamespaceInitialEventsFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Namespace'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.namespaceName),
];
export const k8sNamespaceGetEntityName = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => item.namespaceName || '';
export const k8sNamespaceInitialLogTracesFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.namespaceName),
];
export const k8sNamespaceGetEntityName = (item: K8sNamespacesData): string =>
item.namespaceName;
export const namespaceWidgetInfo = [
{
@@ -87,7 +107,7 @@ export const namespaceWidgetInfo = [
];
export const getNamespaceMetricsQueryPayload = (
namespace: InframonitoringtypesNamespaceRecordDTO,
namespace: K8sNamespacesData,
start: number,
end: number,
dotMetricsEnabled: boolean,

View File

@@ -1,46 +1,37 @@
import { FilePenLine } from '@signozhq/icons';
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { Tooltip } from 'antd';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sNamespacesData, K8sNamespacesListPayload } from './api';
import { FilePenLine } from '@signozhq/icons';
export function getK8sNamespaceRowKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return (
namespace.namespaceName ||
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ||
''
);
export function getK8sNamespaceRowKey(namespace: K8sNamespacesData): string {
return namespace.namespaceName || namespace.meta.k8s_namespace_name;
}
export function getK8sNamespaceItemKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return namespace.namespaceName;
export function getK8sNamespaceItemKey(namespace: K8sNamespacesData): string {
return namespace.meta.k8s_namespace_name;
}
export type NamespaceTableColumnConfig =
TableColumnDef<InframonitoringtypesNamespaceRecordDTO>;
export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
{
id: 'namespaceGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Namespace Group" />,
accessorFn: (row): string => row.namespaceName || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="NAMESPACE GROUP" />,
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -63,7 +54,6 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
<EntityGroupHeader
title="Namespace Name"
icon={<FilePenLine data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/namespaces#namespace-name"
/>
),
accessorFn: (row): string => row.namespaceName || '',
@@ -76,85 +66,49 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'clusterName',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cluster-name">
Cluster Name
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
header: 'Cluster Name',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { default: 150 },
enableSort: false,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value as string}</TanStackTable.Text>
),
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesNamespaceRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cpu-usage-cores">
CPU Usage (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.namespaceCPU,
width: { min: 190 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NAMESPACES}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#mem-usage-wss">
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.namespaceMemory,
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = Number(value);
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listNodes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sNodesList, K8sNodeData } from './api';
import {
getNodeMetricsQueryPayload,
k8sNodeDetailsMetadataConfig,
k8sNodeGetEntityName,
k8sNodeGetSelectedItemExpression,
k8sNodeInitialEventsExpression,
k8sNodeInitialLogTracesExpression,
k8sNodeGetSelectedItemFilters,
k8sNodeInitialEventsFilter,
k8sNodeInitialLogTracesFilter,
nodeWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ function K8sNodesList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch nodes';
return {
type: 'list' as const,
records: [] as InframonitoringtypesNodeRecordDTO[],
total: 0,
error: errMsg,
};
}
const response = await getK8sNodesList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNodeRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listNodes(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
): Promise<{ data: K8sNodeData | null; error?: string | null }> => {
const response = await getK8sNodesList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch node';
return {
data: null,
error: errMsg,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesNodeRecordDTO>
<K8sBaseList<K8sNodeData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NODES}
tableColumns={k8sNodesColumnsConfig}
@@ -127,14 +97,14 @@ function K8sNodesList({
eventCategory={InfraMonitoringEvents.Node}
/>
<K8sBaseDetails<InframonitoringtypesNodeRecordDTO>
<K8sBaseDetails<K8sNodeData>
category={InfraMonitoringEntity.NODES}
eventCategory={InfraMonitoringEvents.Node}
getSelectedItemExpression={k8sNodeGetSelectedItemExpression}
getSelectedItemFilters={k8sNodeGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sNodeGetEntityName}
getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression}
getInitialEventsExpression={k8sNodeInitialEventsExpression}
getInitialLogTracesFilters={k8sNodeInitialLogTracesFilter}
getInitialEventsFilters={k8sNodeInitialEventsFilter}
metadataConfig={k8sNodeDetailsMetadataConfig}
entityWidgetInfo={nodeWidgetInfo}
getEntityQueryPayload={getNodeMetricsQueryPayload}

View File

@@ -0,0 +1,129 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { K8sBaseFilters } from '../Base/types';
export interface K8sNodesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNodeData {
nodeUID: string;
nodeCPUUsage: number;
nodeCPUAllocatable: number;
nodeMemoryUsage: number;
nodeMemoryAllocatable: number;
meta: {
k8s_node_name: string;
k8s_node_uid: string;
k8s_cluster_name: string;
};
}
export interface K8sNodesListResponse {
status: string;
data: {
type: string;
records: K8sNodeData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
// TODO(H4ad): Erase this whole file when migrating to openapi
export const nodesMetaMap = [
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{ dot: 'k8s.node.uid', under: 'k8s_node_uid' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapNodesMeta(
raw: Record<string, unknown>,
): K8sNodeData['meta'] {
const out: Record<string, unknown> = { ...raw };
nodesMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sNodeData['meta'];
}
export const getK8sNodesList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
try {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/nodes/list', requestProps, {
signal,
headers,
});
const payload: K8sNodesListResponse = response.data;
// one-liner to map dot→underscore
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapNodesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,43 +1,67 @@
import { InframonitoringtypesNodeRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sNodeData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sNodeGetSelectedItemExpression = (
export const k8sNodeGetSelectedItemFilters = (
selectedItemId: string,
): string => `k8s.node.name = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => {
return {
op: 'AND',
items: [
{
id: 'k8s_node_name',
key: {
key: 'k8s_node_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
};
};
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNodeRecordDTO>[] =
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNodeData>[] =
[
{ label: 'Node Name', getValue: (p): string => p.nodeName || '' },
{ label: 'Node Name', getValue: (p): string => p.meta.k8s_node_name },
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
];
export const k8sNodeInitialEventsExpression = (
item: InframonitoringtypesNodeRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Node' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.nodeName || '')}`;
export const k8sNodeInitialFilters = [
QUERY_KEYS.K8S_NODE_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
];
export const k8sNodeInitialLogTracesExpression = (
item: InframonitoringtypesNodeRecordDTO,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME} = ${formatValueForExpression(item.nodeName || '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '')}`;
export const k8sNodeInitialEventsFilter = (
item: K8sNodeData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Node'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_node_name),
];
export const k8sNodeGetEntityName = (
item: InframonitoringtypesNodeRecordDTO,
): string => item.nodeName || '';
export const k8sNodeInitialLogTracesFilter = (
item: K8sNodeData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_NODE_NAME, item.meta.k8s_node_name),
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
];
export const k8sNodeGetEntityName = (item: K8sNodeData): string =>
item.meta.k8s_node_name;
export const nodeWidgetInfo = [
{
@@ -83,7 +107,7 @@ export const nodeWidgetInfo = [
];
export const getNodeMetricsQueryPayload = (
node: InframonitoringtypesNodeRecordDTO,
node: K8sNodeData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -198,7 +222,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -237,7 +261,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -276,7 +300,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -315,7 +339,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -354,7 +378,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -427,7 +451,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -466,7 +490,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -505,7 +529,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -544,7 +568,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -583,7 +607,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -622,7 +646,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -661,7 +685,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -734,7 +758,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -773,7 +797,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -812,7 +836,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -898,7 +922,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -937,7 +961,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -976,7 +1000,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1062,7 +1086,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1142,7 +1166,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1222,7 +1246,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1308,7 +1332,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1394,7 +1418,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1433,7 +1457,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1472,7 +1496,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1545,7 +1569,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1584,7 +1608,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',

View File

@@ -1,56 +1,38 @@
import { Color } from '@signozhq/design-tokens';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { InframonitoringtypesNodeRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
import { TableColumnDef } from 'components/TanStackTableView';
import TanStackTable from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sNodeData, K8sNodesListPayload } from './api';
import { Workflow } from '@signozhq/icons';
export function getK8sNodeRowKey(
node: InframonitoringtypesNodeRecordDTO,
): string {
return (
node.nodeName || node.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_UID] || ''
);
export function getK8sNodeRowKey(node: K8sNodeData): string {
return node.nodeUID || node.meta.k8s_node_uid || node.meta.k8s_node_name;
}
export function getK8sNodeItemKey(
node: InframonitoringtypesNodeRecordDTO,
): string {
return node.nodeName;
export function getK8sNodeItemKey(node: K8sNodeData): string {
return node.meta.k8s_node_name;
}
const NODE_CONDITION_COLORS: Record<string, BadgeColor> = {
ready: 'forest',
not_ready: 'amber',
no_data: 'secondary',
};
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const NODE_CONDITION_LABEL_MAP: Record<string, string> = {
ready: 'Ready',
not_ready: 'Not Ready',
no_data: 'No Data',
};
export type NodeTableColumnConfig =
TableColumnDef<InframonitoringtypesNodeRecordDTO>;
export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
{
id: 'nodeGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Node Group" />,
accessorFn: (row): string => row.nodeName || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="NODE GROUP" />,
accessorFn: (row): string => row.meta.k8s_node_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -73,10 +55,9 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
<EntityGroupHeader
title="Node Name"
icon={<Workflow data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/nodes#node-name"
/>
),
accessorFn: (row): string => row.nodeName || '',
accessorFn: (row): string => row.meta.k8s_node_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -86,146 +67,65 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const nodeName = value as string;
return (
<CellValueTooltip value={nodeName}>
<Tooltip title={nodeName}>
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
id: 'condition',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#node-condition">
Status
</ColumnHeader>
),
accessorFn: (row): string => row.condition,
width: { min: 120 },
enableSort: false,
cell: ({ row, groupMeta }): React.ReactNode => {
if (!groupMeta) {
const color =
NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data;
return (
<Badge color={color} variant="outline">
{NODE_CONDITION_LABEL_MAP[row.condition] || 'Unknown'}
</Badge>
);
}
return (
<GroupedStatusCounts
items={[
{
value: row.nodeCountsByReadiness?.ready ?? 0,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.nodeCountsByReadiness?.notReady ?? 0,
label: 'Not Ready',
color: Color.BG_AMBER_500,
},
]}
/>
);
},
},
{
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
</Tooltip>
);
},
},
{
id: 'clusterName',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cluster-name">
Cluster Name
</ColumnHeader>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
header: 'Cluster Name',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { min: 150, default: 150 },
enableSort: false,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value as string}</TanStackTable.Text>
),
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeCPU,
width: { min: 160, default: 160 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.nodeCPUUsage,
width: { min: 200, default: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NODES}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-alloc-cores">
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
header: 'CPU Alloc (cores)',
accessorFn: (row): number => row.nodeCPUAllocatable,
width: { min: 160, default: 190 },
width: { min: 200, default: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = Number(value);
const cpuAllocatable = value as number;
return (
<ValidateColumnValueWrapper
value={cpuAllocatable}
entity={InfraMonitoringEntity.NODES}
attribute="CPU allocatable metric"
>
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeMemory,
width: { min: 220, default: 220 },
header: 'Memory Usage (WSS)',
accessorFn: (row): number => row.nodeMemoryUsage,
width: { min: 240, default: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -242,14 +142,9 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
},
{
id: 'memory_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-allocatable">
Memory
<br /> Allocatable
</ColumnHeader>
),
header: 'Memory Allocatable',
accessorFn: (row): number => row.nodeMemoryAllocatable,
width: { min: 200, default: 200 },
width: { min: 240, default: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listPods } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { getK8sPodsList, K8sPodsData } from './api';
import {
getPodMetricsQueryPayload,
k8sPodDetailsMetadataConfig,
k8sPodGetEntityName,
k8sPodGetSelectedItemExpression,
k8sPodInitialEventsExpression,
k8sPodInitialLogTracesExpression,
k8sPodGetSelectedItemFilters,
k8sPodInitialEventsFilter,
k8sPodInitialLogTracesFilter,
podWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ function K8sPodsList({
}: {
controlListPrefix?: React.ReactNode;
}): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
warning: data.warning,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch pods';
return {
type: 'list' as const,
records: [] as InframonitoringtypesPodRecordDTO[],
total: 0,
error: errMsg,
};
}
const response = await getK8sPodsList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
rawData: response.payload?.data,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesPodRecordDTO | null;
error?: string | null;
}> => {
try {
const response = await listPods(
{
filter: { expression: filters.filter.expression },
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
);
): Promise<{ data: K8sPodsData | null; error?: string | null }> => {
const response = await getK8sPodsList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
},
signal,
undefined,
dotMetricsEnabled,
);
return {
data: response.data.records.length > 0 ? response.data.records[0] : null,
};
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Failed to fetch pod';
return {
data: null,
error: errMsg,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesPodRecordDTO>
<K8sBaseList<K8sPodsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.PODS}
tableColumns={k8sPodColumnsConfig}
@@ -127,14 +97,14 @@ function K8sPodsList({
eventCategory={InfraMonitoringEvents.Pod}
/>
<K8sBaseDetails<InframonitoringtypesPodRecordDTO>
<K8sBaseDetails<K8sPodsData>
category={InfraMonitoringEntity.PODS}
eventCategory={InfraMonitoringEvents.Pod}
getSelectedItemExpression={k8sPodGetSelectedItemExpression}
getSelectedItemFilters={k8sPodGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sPodGetEntityName}
getInitialLogTracesExpression={k8sPodInitialLogTracesExpression}
getInitialEventsExpression={k8sPodInitialEventsExpression}
getInitialLogTracesFilters={k8sPodInitialLogTracesFilter}
getInitialEventsFilters={k8sPodInitialEventsFilter}
metadataConfig={k8sPodDetailsMetadataConfig}
entityWidgetInfo={podWidgetInfo}
getEntityQueryPayload={getPodMetricsQueryPayload}

View File

@@ -0,0 +1,148 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { UnderscoreToDotMap } from 'api/utils';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { K8sBaseFilters } from '../Base/types';
// TODO(H4ad): Erase this whole file when migrating to openapi
export const podsMetaMap = [
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
// clone everything
const out: Record<string, unknown> = { ...raw };
// overlay only the dot→under mappings
podsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sPodsData['meta'];
}
export interface TimeSeriesValue {
timestamp: number;
value: string;
}
export interface TimeSeries {
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: TimeSeriesValue[];
}
export interface K8sPodsData {
podUID: string;
podCPU: number;
podCPURequest: number;
podCPULimit: number;
podMemory: number;
podMemoryRequest: number;
podMemoryLimit: number;
restartCount: number;
meta: {
k8s_cronjob_name: string;
k8s_daemonset_name: string;
k8s_deployment_name: string;
k8s_job_name: string;
k8s_namespace_name: string;
k8s_node_name: string;
k8s_pod_name: string;
k8s_pod_uid: string;
k8s_statefulset_name: string;
k8s_cluster_name: string;
};
countByPhase: {
pending: number;
running: number;
succeeded: number;
failed: number;
unknown: number;
};
}
export interface K8sPodsListResponse {
status: string;
data: {
type: string;
records: K8sPodsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sPodsList = async (
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
try {
const requestProps = dotMetricsEnabled
? {
...props,
filters: {
...props.filters,
items: props.filters?.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) {
return acc;
}
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/pods/list', requestProps, {
signal,
headers,
});
const payload: K8sPodsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapPodsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

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