Compare commits

..

1 Commits

Author SHA1 Message Date
Tushar Vats
87543f9123 fix: handling missing keys metadata 2026-07-13 06:08:57 +05:30
380 changed files with 8129 additions and 16211 deletions

View File

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

View File

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

View File

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

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

View File

@@ -75,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",
@@ -238,18 +238,10 @@
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"brace-expansion": "^2.0.3",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"js-cookie": "^3.0.7",
"tmp": "0.2.7",
"vite": "npm:rolldown-vite@7.3.1",
"dompurify": "3.4.11",
"js-yaml@3": "3.15.0",
"js-yaml@4": "4.2.0",
"yaml@1": "1.10.3",
"react-router@6": "6.30.4",
"markdown-it": "14.2.0",
"mdast-util-to-hast@13": "13.2.1",
"protocol-buffers-schema": "3.6.1"
"vite": "npm:rolldown-vite@7.3.1"
}
}

152
frontend/pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,12 @@ export enum LOCALSTORAGE {
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
QUICK_FILTERS_WIDTH_TRACES = 'QUICK_FILTERS_WIDTH_TRACES',
QUICK_FILTERS_WIDTH_METER = 'QUICK_FILTERS_WIDTH_METER',
QUICK_FILTERS_WIDTH_API_MONITORING = 'QUICK_FILTERS_WIDTH_API_MONITORING',
QUICK_FILTERS_WIDTH_EXCEPTIONS = 'QUICK_FILTERS_WIDTH_EXCEPTIONS',
QUICK_FILTERS_WIDTH_INFRA = 'QUICK_FILTERS_WIDTH_INFRA',
FUNNEL_STEPS = 'FUNNEL_STEPS',
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',

View File

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

View File

@@ -163,12 +163,23 @@
}
&.filter-visible {
// Width is owned by ResizableBox (inline style); this section is the
// ResizableBox root, so it stays position: relative for the drag handle.
.api-quick-filter-left-section {
width: 260px;
.resizable-box__content {
display: flex;
flex-direction: column;
}
.quick-filters-container {
flex: 1;
min-height: 0;
}
}
.api-module-right-section {
width: calc(100% - 260px);
flex: 1;
min-width: 0;
}
}
}

View File

@@ -4,21 +4,49 @@ import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { LOCALSTORAGE } from 'constants/localStorage';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import DomainList from './Domains/DomainList';
import './Explorer.styles.scss';
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
function Explorer(): JSX.Element {
useEffect(() => {
logEvent('API Monitoring: Landing page visited', {});
}, []);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_API_MONITORING,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className={cx('api-monitoring-page', 'filter-visible')}>
<section className="api-quick-filter-left-section">
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className="api-quick-filter-left-section"
handleTestId="quick-filters-resize-handle"
>
<QuickFilters
className="qf-api-monitoring"
source={QuickFiltersSource.API_MONITORING}
@@ -27,7 +55,7 @@ function Explorer(): JSX.Element {
showQueryName={false}
handleFilterVisibilityChange={(): void => {}}
/>
</section>
</ResizableBox>
<DomainList />
</div>
</Sentry.ErrorBoundary>

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,4 +0,0 @@
.wrapper :global(button[data-color]) {
--checkbox-checked-background: var(--series-color);
--checkbox-border-color: var(--series-color);
}

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import K8sBaseDetails from 'container/InfraMonitoringK8s/Base/K8sBaseDetails';
import { K8sBaseList } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
@@ -16,6 +17,8 @@ import {
} from 'container/InfraMonitoringK8s/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -40,8 +43,21 @@ import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
const [, setCurrentPage] = useInfraMonitoringPageListing();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
@@ -139,7 +155,18 @@ function Hosts(): JSX.Element {
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
@@ -156,7 +183,7 @@ function Hosts(): JSX.Element {
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</div>
</ResizableBox>
)}
<div
className={`${styles.listContainer}${

View File

@@ -44,26 +44,20 @@
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
:global(.ant-collapse-header) {
@@ -142,7 +136,9 @@
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.quickFiltersToggleContainer {

View File

@@ -1,37 +1,36 @@
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 { LOCALSTORAGE } from 'constants/localStorage';
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 { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
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 {
@@ -44,137 +43,100 @@ import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
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 {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
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
@@ -193,7 +155,18 @@ function Hosts(): JSX.Element {
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
@@ -208,17 +181,17 @@ function Hosts(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</div>
</ResizableBox>
)}
<div
className={`${styles.listContainer}${
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList<InframonitoringtypesHostRecordDTO>
<K8sBaseList
controlListPrefix={controlListPrefix}
leftFilters={<StatusFilter />}
entity={InfraMonitoringEntity.HOSTS}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
@@ -232,11 +205,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

@@ -25,31 +25,6 @@
}
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
}
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
@@ -66,6 +41,24 @@
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
.quickFiltersContainer {
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
@@ -99,7 +92,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
@@ -114,25 +107,42 @@
.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 {
max-width: calc(100% - 280px);
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.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

@@ -44,26 +44,15 @@
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
:global(.quick-filters) {
@@ -128,7 +117,9 @@
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.categorySelectorSection {
@@ -219,8 +210,16 @@
.quickFiltersSection {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
&::-webkit-scrollbar {
width: 0.1rem;
}

View File

@@ -6,6 +6,7 @@ import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
@@ -22,6 +23,8 @@ import {
Workflow,
} from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { FeatureKeys } from '../../constants/features';
@@ -56,9 +59,23 @@ import K8sVolumesList from './Volumes/K8sVolumesList';
import styles from './InfraMonitoringK8s.module.scss';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const [, setGroupBy] = useInfraMonitoringGroupBy();
@@ -212,7 +229,18 @@ export default function InfraMonitoringK8s(): JSX.Element {
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.categorySelectorSection}>
<div className={styles.sectionHeader} data-type="resource">
<Typography.Text className={styles.sectionLabel}>
@@ -265,7 +293,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
/>
)}
</div>
</div>
</ResizableBox>
)}
<div

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,98 +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: 180 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.currentNodes,
label: 'Current',
color: Color.BG_FOREST_500,
},
{
value: row.desiredNodes,
label: 'Desired',
color: Color.BG_ROBIN_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 (
@@ -187,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 (
@@ -212,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;
@@ -264,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 (
@@ -289,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 (
@@ -312,52 +238,4 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
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>
);
},
},
];

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

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

@@ -25,31 +25,6 @@
}
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
}
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
@@ -66,6 +41,19 @@
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
.quickFiltersContainer {
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
:global(.quick-filters) {
overflow-y: auto;
@@ -85,7 +73,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
@@ -100,12 +88,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;
@@ -130,7 +117,9 @@
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.categorySelectorSection {
@@ -221,8 +210,16 @@
.quickFiltersSection {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
@@ -235,7 +232,3 @@
background: var(--accent-primary);
}
}
.k8SOpenQuickFilters {
grid-area: quickFilters;
}

View File

@@ -2,11 +2,13 @@ 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 { LOCALSTORAGE } from 'constants/localStorage';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
ArrowUpDown,
ArrowUpToLine,
@@ -21,7 +23,9 @@ import {
Workflow,
} from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataSource } from 'types/common/queryBuilder';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
@@ -42,9 +46,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 +58,77 @@ 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';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
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 +196,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 +209,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
return (
<>
{!showFilters && (
<div className={styles.k8SOpenQuickFilters}>
<div>
<Button
className="periscope-btn ghost"
type="text"
@@ -220,7 +229,18 @@ export default function InfraMonitoringK8s(): JSX.Element {
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.categorySelectorSection}>
<div className={styles.sectionHeader} data-type="resource">
<Typography.Text className={styles.sectionLabel}>
@@ -269,10 +289,11 @@ export default function InfraMonitoringK8s(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
)}
</div>
</div>
</ResizableBox>
)}
<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;

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