mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-14 02:20:32 +01:00
Compare commits
1 Commits
hotfix/spa
...
ci/better-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bfd604aa4 |
6
.github/workflows/integrationci.yaml
vendored
6
.github/workflows/integrationci.yaml
vendored
@@ -48,11 +48,7 @@ jobs:
|
||||
- logspipelines
|
||||
- passwordauthn
|
||||
- preference
|
||||
- querierlogs
|
||||
- queriertraces
|
||||
- queriermetrics
|
||||
- querierscalar
|
||||
- queriercommon
|
||||
- querier
|
||||
- rawexportdata
|
||||
- role
|
||||
- rootuser
|
||||
|
||||
41
.github/workflows/jsci.yaml
vendored
41
.github/workflows/jsci.yaml
vendored
@@ -102,3 +102,44 @@ jobs:
|
||||
run: |
|
||||
cd frontend && pnpm generate:config:web-settings
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated web settings types. Run pnpm generate:config:web-settings in frontend/ locally and commit."; exit 1)
|
||||
code-quality:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: install-pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
- name: install-frontend
|
||||
run: cd frontend && pnpm install
|
||||
- name: get-new-files
|
||||
id: new-files
|
||||
run: |
|
||||
cd frontend
|
||||
NEW_TS=$(git diff --name-only --diff-filter=A origin/main...HEAD -- src/ | grep -E '\.(ts|tsx)$' | tr '\n' ' ' || true)
|
||||
NEW_SCSS=$(git diff --name-only --diff-filter=A origin/main...HEAD -- src/ | grep -E '\.scss$' | tr '\n' ' ' || true)
|
||||
echo "ts=$NEW_TS" >> $GITHUB_OUTPUT
|
||||
echo "scss=$NEW_SCSS" >> $GITHUB_OUTPUT
|
||||
echo "New TS files: $NEW_TS"
|
||||
echo "New SCSS files: $NEW_SCSS"
|
||||
- name: lint-new-ts-files
|
||||
if: steps.new-files.outputs.ts != ''
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm oxlint -c .oxlintrc.strict.json ${{ steps.new-files.outputs.ts }}
|
||||
- name: lint-new-scss-files
|
||||
if: steps.new-files.outputs.scss != ''
|
||||
run: |
|
||||
cd frontend
|
||||
pnpm stylelint --config .stylelintrc.strict.cjs ${{ steps.new-files.outputs.scss }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1494,7 +1494,7 @@ components:
|
||||
- cosmosdb
|
||||
- cassandradb
|
||||
- redis
|
||||
- cloudsql_postgres
|
||||
- cloudsql
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -3148,8 +3148,6 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3182,7 +3180,6 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
- pinned
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2:
|
||||
@@ -3196,8 +3193,6 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3228,7 +3223,6 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2Spec:
|
||||
properties:
|
||||
@@ -4195,7 +4189,6 @@ components:
|
||||
- namespaces
|
||||
- clusters
|
||||
- volumes
|
||||
- kube_containers
|
||||
type: string
|
||||
InframonitoringtypesChecks:
|
||||
properties:
|
||||
@@ -4301,158 +4294,6 @@ components:
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesContainerCountsByReady:
|
||||
properties:
|
||||
notReady:
|
||||
type: integer
|
||||
ready:
|
||||
type: integer
|
||||
required:
|
||||
- ready
|
||||
- notReady
|
||||
type: object
|
||||
InframonitoringtypesContainerCountsByStatus:
|
||||
properties:
|
||||
completed:
|
||||
type: integer
|
||||
containerCannotRun:
|
||||
type: integer
|
||||
containerCreating:
|
||||
type: integer
|
||||
crashLoopBackOff:
|
||||
type: integer
|
||||
createContainerConfigError:
|
||||
type: integer
|
||||
errImagePull:
|
||||
type: integer
|
||||
error:
|
||||
type: integer
|
||||
imagePullBackOff:
|
||||
type: integer
|
||||
oomKilled:
|
||||
type: integer
|
||||
running:
|
||||
type: integer
|
||||
terminated:
|
||||
type: integer
|
||||
unknown:
|
||||
type: integer
|
||||
waiting:
|
||||
type: integer
|
||||
required:
|
||||
- running
|
||||
- waiting
|
||||
- terminated
|
||||
- crashLoopBackOff
|
||||
- imagePullBackOff
|
||||
- errImagePull
|
||||
- createContainerConfigError
|
||||
- containerCreating
|
||||
- oomKilled
|
||||
- completed
|
||||
- error
|
||||
- containerCannotRun
|
||||
- unknown
|
||||
type: object
|
||||
InframonitoringtypesContainerReady:
|
||||
enum:
|
||||
- ready
|
||||
- not_ready
|
||||
- no_data
|
||||
type: string
|
||||
InframonitoringtypesContainerRecord:
|
||||
properties:
|
||||
containerCountsByReady:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerCountsByReady'
|
||||
containerCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerCountsByStatus'
|
||||
containerName:
|
||||
type: string
|
||||
cpu:
|
||||
format: double
|
||||
type: number
|
||||
cpuLimitUtilization:
|
||||
format: double
|
||||
type: number
|
||||
cpuRequestUtilization:
|
||||
format: double
|
||||
type: number
|
||||
memory:
|
||||
format: double
|
||||
type: number
|
||||
memoryLimitUtilization:
|
||||
format: double
|
||||
type: number
|
||||
memoryRequestUtilization:
|
||||
format: double
|
||||
type: number
|
||||
meta:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
podUID:
|
||||
type: string
|
||||
ready:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerReady'
|
||||
restarts:
|
||||
format: int64
|
||||
type: integer
|
||||
status:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
|
||||
required:
|
||||
- podUID
|
||||
- containerName
|
||||
- status
|
||||
- containerCountsByStatus
|
||||
- ready
|
||||
- containerCountsByReady
|
||||
- restarts
|
||||
- cpu
|
||||
- cpuRequestUtilization
|
||||
- cpuLimitUtilization
|
||||
- memory
|
||||
- memoryRequestUtilization
|
||||
- memoryLimitUtilization
|
||||
- meta
|
||||
type: object
|
||||
InframonitoringtypesContainerStatus:
|
||||
enum:
|
||||
- running
|
||||
- waiting
|
||||
- terminated
|
||||
- crashloopbackoff
|
||||
- imagepullbackoff
|
||||
- errimagepull
|
||||
- createcontainerconfigerror
|
||||
- containercreating
|
||||
- oomkilled
|
||||
- completed
|
||||
- error
|
||||
- containercannotrun
|
||||
- unknown
|
||||
- no_data
|
||||
type: string
|
||||
InframonitoringtypesContainers:
|
||||
properties:
|
||||
endTimeBeforeRetention:
|
||||
type: boolean
|
||||
records:
|
||||
items:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainerRecord'
|
||||
type: array
|
||||
total:
|
||||
type: integer
|
||||
type:
|
||||
$ref: '#/components/schemas/InframonitoringtypesResponseType'
|
||||
warning:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
|
||||
required:
|
||||
- type
|
||||
- records
|
||||
- total
|
||||
- endTimeBeforeRetention
|
||||
type: object
|
||||
InframonitoringtypesDaemonSetRecord:
|
||||
properties:
|
||||
currentNodes:
|
||||
@@ -4484,14 +4325,10 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
misscheduledNodes:
|
||||
type: integer
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
readyNodes:
|
||||
type: integer
|
||||
required:
|
||||
- daemonSetName
|
||||
- daemonSetCPU
|
||||
@@ -4502,8 +4339,6 @@ components:
|
||||
- daemonSetMemoryLimit
|
||||
- desiredNodes
|
||||
- currentNodes
|
||||
- readyNodes
|
||||
- misscheduledNodes
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
@@ -5133,32 +4968,6 @@ components:
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesPostableContainers:
|
||||
properties:
|
||||
end:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
groupBy:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
orderBy:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
required:
|
||||
- start
|
||||
- end
|
||||
- limit
|
||||
type: object
|
||||
InframonitoringtypesPostableDaemonSets:
|
||||
properties:
|
||||
end:
|
||||
@@ -7406,11 +7215,9 @@ components:
|
||||
op:
|
||||
$ref: '#/components/schemas/RuletypesCompareOperator'
|
||||
recoveryTarget:
|
||||
format: double
|
||||
nullable: true
|
||||
type: number
|
||||
target:
|
||||
format: double
|
||||
nullable: true
|
||||
type: number
|
||||
targetUnit:
|
||||
@@ -7694,7 +7501,6 @@ components:
|
||||
selectedQueryName:
|
||||
type: string
|
||||
target:
|
||||
format: double
|
||||
nullable: true
|
||||
type: number
|
||||
targetUnit:
|
||||
@@ -7998,15 +7804,6 @@ components:
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableSpanMappers:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapper'
|
||||
type: array
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableTraceAggregations:
|
||||
properties:
|
||||
aggregations:
|
||||
@@ -8159,7 +7956,7 @@ components:
|
||||
type: boolean
|
||||
fieldContext:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
groupId:
|
||||
group_id:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
@@ -8172,7 +7969,7 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- groupId
|
||||
- group_id
|
||||
- name
|
||||
- fieldContext
|
||||
- config
|
||||
@@ -13751,7 +13548,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -16014,24 +15811,21 @@ paths:
|
||||
daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row
|
||||
also reports the latest known node-level counters from kube-state-metrics:
|
||||
desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the
|
||||
daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes,
|
||||
the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes,
|
||||
the number of nodes running at least one ready daemon pod) and misscheduledNodes
|
||||
(k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon
|
||||
pod but not supposed to) — note these are node counts, not pod counts. It
|
||||
also reports per-group podCountsByPhase ({ pending, running, succeeded, failed,
|
||||
unknown } from each pod''s latest k8s.pod.phase value). Each daemonset includes
|
||||
metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name).
|
||||
The response type is ''list'' for the default k8s.daemonset.name grouping
|
||||
or ''grouped_list'' for custom groupBy keys; in both modes every row aggregates
|
||||
pods owned by daemonsets in the group. Supports filtering via a filter expression,
|
||||
custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
|
||||
/ memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes,
|
||||
daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes,
|
||||
the number of nodes the daemonset currently runs on) — note these are node
|
||||
counts, not pod counts. It also reports per-group podCountsByPhase ({ pending,
|
||||
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
|
||||
value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name,
|
||||
k8s.cluster.name). The response type is ''list'' for the default k8s.daemonset.name
|
||||
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
|
||||
row aggregates pods owned by daemonsets in the group. Supports filtering via
|
||||
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
|
||||
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
|
||||
and pagination via offset/limit. Also reports whether the requested time range
|
||||
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
|
||||
daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest,
|
||||
daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes)
|
||||
return -1 as a sentinel when no data is available for that field.'
|
||||
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
|
||||
when no data is available for that field.'
|
||||
operationId: ListDaemonSets
|
||||
requestBody:
|
||||
content:
|
||||
@@ -16305,86 +16099,6 @@ paths:
|
||||
summary: List Jobs for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/kube_containers:
|
||||
post:
|
||||
deprecated: false
|
||||
description: 'Returns a paginated list of Kubernetes containers with key kubeletstats
|
||||
metrics: CPU usage (cores), CPU request/limit utilization, memory working
|
||||
set, and memory request/limit utilization. Each container also reports health
|
||||
signals from the k8s_cluster receiver: status (kubectl-style display status
|
||||
derived from k8s.container.status.state + k8s.container.status.reason), restarts
|
||||
(absolute count from k8s.container.restarts), and ready (ready/not_ready from
|
||||
k8s.container.ready). The row identity is (k8s.pod.uid, k8s.container.name),
|
||||
stable across container restarts. Each container includes metadata attributes
|
||||
(k8s.container.name, k8s.pod.name, container.image.name, container.image.tag,
|
||||
k8s.namespace.name, k8s.node.name, k8s.cluster.name, and workload owner such
|
||||
as deployment/statefulset/daemonset/job). The response type is ''list'' for
|
||||
the default (k8s.pod.uid, k8s.container.name) grouping (each row is one container
|
||||
with its current status and ready state) or ''grouped_list'' for custom groupBy
|
||||
keys (each row aggregates containers in the group with per-status counts under
|
||||
containerCountsByStatus, per-readiness counts under containerCountsByReady,
|
||||
and restarts as the group sum). Status requires the optional k8s.container.status.state
|
||||
and k8s.container.status.reason metrics; when either is missing, status is
|
||||
omitted and a warning is returned while restarts and ready are still computed.
|
||||
Supports filtering via a filter expression, custom groupBy, ordering by any
|
||||
of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit),
|
||||
and pagination via offset/limit. Also reports whether the requested time range
|
||||
falls before the data retention boundary. Numeric metric fields (cpu, cpuRequestUtilization,
|
||||
cpuLimitUtilization, memory, memoryRequestUtilization, memoryLimitUtilization)
|
||||
and restarts return -1 as a sentinel when no data is available for that field.'
|
||||
operationId: ListContainers
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPostableContainers'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/InframonitoringtypesContainers'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List Kubernetes Containers for Infra Monitoring
|
||||
tags:
|
||||
- inframonitoring
|
||||
/api/v2/infra_monitoring/namespaces:
|
||||
post:
|
||||
deprecated: false
|
||||
@@ -18195,16 +17909,6 @@ paths:
|
||||
public dashboard. The panel is addressed by its key in spec.panels.
|
||||
operationId: GetPublicDashboardPanelQueryRangeV2
|
||||
parameters:
|
||||
- in: query
|
||||
name: startTime
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: endTime
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
|
||||
@@ -116,15 +116,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
infraMonitoringV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseInfraMonitoringV2, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseInfraMonitoringV2.String()),
|
||||
Active: infraMonitoringV2,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,18 @@
|
||||
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
|
||||
"signoz/no-dashboard-fetch-outside-root": "error",
|
||||
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
|
||||
"signoz/no-raw-html-text-elements": "warn",
|
||||
// Bans raw span/p/h1-6 elements - use Typography component instead
|
||||
"signoz/no-multiple-components": "warn",
|
||||
// Bans multiple component exports per file - split into separate files
|
||||
"signoz/no-inline-helpers": "warn",
|
||||
// Bans too many helper functions in component files - move to utils.ts
|
||||
"signoz/max-component-lines": ["warn", { "max": 300 }],
|
||||
// Warns when component files exceed max lines - split into smaller components
|
||||
"signoz/no-getByText": "warn",
|
||||
// Bans getByText in tests - use getByTestId or getByRole instead
|
||||
"signoz/no-fireEvent": "warn",
|
||||
// Bans fireEvent in tests - use userEvent instead
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
|
||||
18
frontend/.oxlintrc.strict.json
Normal file
18
frontend/.oxlintrc.strict.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["./.oxlintrc.json"],
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"es2021": true,
|
||||
"browser": true,
|
||||
"jest": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"signoz/no-raw-html-text-elements": "error",
|
||||
"signoz/no-multiple-components": "error",
|
||||
"signoz/no-inline-helpers": "error",
|
||||
"signoz/no-getByText": "error",
|
||||
"signoz/no-fireEvent": "error"
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,17 @@ module.exports = {
|
||||
),
|
||||
path.join(__dirname, 'stylelint-rules/css-modules/prefer-css-variables.js'),
|
||||
path.join(__dirname, 'stylelint-rules/css-modules/class-name-pattern.js'),
|
||||
path.join(__dirname, 'stylelint-rules/no-font-properties.js'),
|
||||
path.join(__dirname, 'stylelint-rules/no-primitive-color-tokens.js'),
|
||||
path.join(__dirname, 'stylelint-rules/no-styles-scss.js'),
|
||||
],
|
||||
customSyntax: 'postcss-scss',
|
||||
rules: {
|
||||
// Applies to all SCSS files
|
||||
'local/no-unsupported-asset-url': true,
|
||||
'local/no-font-properties': [true, { severity: 'warning' }],
|
||||
'local/no-primitive-color-tokens': [true, { severity: 'warning' }],
|
||||
'local/no-styles-scss': [true, { severity: 'warning' }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
14
frontend/.stylelintrc.strict.cjs
Normal file
14
frontend/.stylelintrc.strict.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Strict config for new files - rules that are warnings in base config become errors here
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
const baseConfig = require('./.stylelintrc.cjs');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
rules: {
|
||||
...baseConfig.rules,
|
||||
// Override warnings to errors for new files
|
||||
'local/no-font-properties': true, // error (no severity = error)
|
||||
'local/no-primitive-color-tokens': true,
|
||||
'local/no-styles-scss': true,
|
||||
},
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"lint:generated": "oxlint ./src/api/generated --fix",
|
||||
"lint:fix": "oxlint ./src --fix",
|
||||
"lint:styles": "stylelint \"src/**/*.scss\"",
|
||||
"lint:strict": "oxlint -c .oxlintrc.strict.json",
|
||||
"lint:styles:strict": "stylelint --config .stylelintrc.strict.cjs",
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
@@ -75,11 +77,10 @@
|
||||
"crypto-js": "4.2.0",
|
||||
"d3-hierarchy": "3.1.2",
|
||||
"dayjs": "^1.10.7",
|
||||
"dompurify": "3.4.11",
|
||||
"dompurify": "3.4.0",
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
"history": "4.10.1",
|
||||
"html-to-image": "1.11.13",
|
||||
"http-status-codes": "2.3.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
@@ -238,18 +239,10 @@
|
||||
"prismjs": "1.30.0",
|
||||
"got": "11.8.5",
|
||||
"form-data": "4.0.6",
|
||||
"brace-expansion": "^2.0.3",
|
||||
"brace-expansion": "^2.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"js-cookie": "^3.0.7",
|
||||
"tmp": "0.2.7",
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
"dompurify": "3.4.11",
|
||||
"js-yaml@3": "3.15.0",
|
||||
"js-yaml@4": "4.2.0",
|
||||
"yaml@1": "1.10.3",
|
||||
"react-router@6": "6.30.4",
|
||||
"markdown-it": "14.2.0",
|
||||
"mdast-util-to-hast@13": "13.2.1",
|
||||
"protocol-buffers-schema": "3.6.1"
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
71
frontend/plugins/rules/max-component-lines.mjs
Normal file
71
frontend/plugins/rules/max-component-lines.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Rule: max-component-lines
|
||||
*
|
||||
* Warns when a component file exceeds configured line limit.
|
||||
* Large components should be split into smaller ones.
|
||||
*
|
||||
* Config: ["warn", { "max": 300 }]
|
||||
* Default: 300 lines
|
||||
*
|
||||
* This rule should always be "warn" (never "error").
|
||||
*/
|
||||
|
||||
const DEFAULT_MAX_LINES = 300;
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Warn when component files exceed a line limit',
|
||||
category: 'Code Quality',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
max: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const options = context.options?.[0] || {};
|
||||
const maxLines = options.max || DEFAULT_MAX_LINES;
|
||||
const filename = context.getFilename();
|
||||
|
||||
// Only check .tsx files (component files)
|
||||
if (!/\.tsx$/.test(filename)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Skip test files
|
||||
if (/\.(test|spec)\.tsx$/.test(filename)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Skip utility/type files
|
||||
if (
|
||||
/\/(utils|helpers|constants|types|hooks)\.(ts|tsx)$/.test(filename) ||
|
||||
/\/(utils|helpers|constants|types)\//.test(filename)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const lines = sourceCode.lines?.length || sourceCode.getText().split('\n').length;
|
||||
|
||||
if (lines > maxLines) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Component file has ${lines} lines (max ${maxLines}). Consider splitting into smaller components or extracting logic into hooks/utils.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
48
frontend/plugins/rules/no-fireEvent.mjs
Normal file
48
frontend/plugins/rules/no-fireEvent.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Rule: no-fireEvent
|
||||
*
|
||||
* Bans fireEvent in test files. Use userEvent instead for more realistic
|
||||
* user interaction simulation.
|
||||
*
|
||||
* BAD:
|
||||
* fireEvent.click(button)
|
||||
* fireEvent.change(input, { target: { value: 'test' } })
|
||||
*
|
||||
* GOOD:
|
||||
* await userEvent.click(button)
|
||||
* await userEvent.type(input, 'test')
|
||||
*/
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
// fireEvent.click(...), fireEvent.change(...)
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
node.object.type === 'Identifier' &&
|
||||
node.object.name === 'fireEvent'
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"Avoid 'fireEvent'. Use userEvent from @testing-library/user-event for more realistic user interactions.",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// import { fireEvent } from '@testing-library/react'
|
||||
ImportSpecifier(node) {
|
||||
if (
|
||||
node.imported.type === 'Identifier' &&
|
||||
node.imported.name === 'fireEvent'
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"Avoid importing 'fireEvent'. Use userEvent from @testing-library/user-event instead.",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
53
frontend/plugins/rules/no-getByText.mjs
Normal file
53
frontend/plugins/rules/no-getByText.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Rule: no-getByText
|
||||
*
|
||||
* Bans getByText, queryByText, findByText, getAllByText, queryAllByText, findAllByText
|
||||
* in test files. Use getByTestId or getByRole instead.
|
||||
*
|
||||
* BAD:
|
||||
* screen.getByText('Submit')
|
||||
* getByText('Submit')
|
||||
*
|
||||
* GOOD:
|
||||
* screen.getByTestId('submit-button')
|
||||
* screen.getByRole('button', { name: 'Submit' })
|
||||
*/
|
||||
|
||||
const BANNED_METHODS = new Set([
|
||||
'getByText',
|
||||
'queryByText',
|
||||
'findByText',
|
||||
'getAllByText',
|
||||
'queryAllByText',
|
||||
'findAllByText',
|
||||
]);
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
let methodName = null;
|
||||
|
||||
// screen.getByText(...) or result.getByText(...)
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier'
|
||||
) {
|
||||
methodName = node.callee.property.name;
|
||||
}
|
||||
|
||||
// Direct getByText(...) from destructured import
|
||||
if (node.callee.type === 'Identifier') {
|
||||
methodName = node.callee.name;
|
||||
}
|
||||
|
||||
if (methodName && BANNED_METHODS.has(methodName)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Avoid '${methodName}'. Use getByTestId or getByRole instead for more stable selectors.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
161
frontend/plugins/rules/no-inline-helpers.mjs
Normal file
161
frontend/plugins/rules/no-inline-helpers.mjs
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Rule: no-inline-helpers
|
||||
*
|
||||
* Bans too many helper functions in component files.
|
||||
* Helper functions should be moved to utils.ts or specialized files.
|
||||
*
|
||||
* Exempt:
|
||||
* - Hooks (use* functions)
|
||||
* - Exported functions (they're intentionally in the file)
|
||||
* - Files named utils.ts, helpers.ts, constants.ts
|
||||
*
|
||||
* Threshold: More than 3 private helper functions triggers warning.
|
||||
*/
|
||||
|
||||
const MAX_HELPERS = 3;
|
||||
|
||||
function isHookName(name) {
|
||||
return /^use[A-Z]/.test(name);
|
||||
}
|
||||
|
||||
function isComponentName(name) {
|
||||
return /^[A-Z]/.test(name);
|
||||
}
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
// Skip utility files
|
||||
if (
|
||||
/\/(utils|helpers|constants|types|hooks)\.(ts|tsx|js|jsx)$/.test(filename) ||
|
||||
/\/(utils|helpers|constants|types)\//.test(filename)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Skip test files
|
||||
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const privateHelpers = [];
|
||||
const exportedNames = new Set();
|
||||
let hasComponent = false;
|
||||
|
||||
return {
|
||||
// Track exported names
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration) {
|
||||
if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
|
||||
exportedNames.add(node.declaration.id.name);
|
||||
if (isComponentName(node.declaration.id.name)) {
|
||||
hasComponent = true;
|
||||
}
|
||||
}
|
||||
if (node.declaration.type === 'VariableDeclaration') {
|
||||
for (const decl of node.declaration.declarations) {
|
||||
if (decl.id.type === 'Identifier') {
|
||||
exportedNames.add(decl.id.name);
|
||||
if (isComponentName(decl.id.name)) {
|
||||
hasComponent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (
|
||||
node.declaration.type === 'FunctionDeclaration' &&
|
||||
node.declaration.id &&
|
||||
isComponentName(node.declaration.id.name)
|
||||
) {
|
||||
hasComponent = true;
|
||||
}
|
||||
if (node.declaration.type === 'Identifier') {
|
||||
exportedNames.add(node.declaration.name);
|
||||
}
|
||||
},
|
||||
|
||||
// Track private function declarations
|
||||
FunctionDeclaration(node) {
|
||||
if (!node.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = node.id.name;
|
||||
|
||||
// Will check if exported later
|
||||
if (
|
||||
!isHookName(name) &&
|
||||
!isComponentName(name) &&
|
||||
node.parent.type !== 'ExportNamedDeclaration' &&
|
||||
node.parent.type !== 'ExportDefaultDeclaration'
|
||||
) {
|
||||
privateHelpers.push({ name, node });
|
||||
}
|
||||
},
|
||||
|
||||
// Track private arrow/function expressions
|
||||
VariableDeclaration(node) {
|
||||
// Skip if this is an export
|
||||
if (
|
||||
node.parent.type === 'ExportNamedDeclaration' ||
|
||||
node.parent.type === 'Program'
|
||||
) {
|
||||
for (const decl of node.declarations) {
|
||||
if (decl.id.type !== 'Identifier') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = decl.id.name;
|
||||
const init = decl.init;
|
||||
|
||||
// Only track functions
|
||||
if (
|
||||
!init ||
|
||||
(init.type !== 'ArrowFunctionExpression' &&
|
||||
init.type !== 'FunctionExpression')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip hooks, components, and exports
|
||||
if (
|
||||
!isHookName(name) &&
|
||||
!isComponentName(name) &&
|
||||
node.parent.type !== 'ExportNamedDeclaration'
|
||||
) {
|
||||
privateHelpers.push({ name, node: decl });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
// Only check files with components
|
||||
if (!hasComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out any that ended up exported
|
||||
const actualPrivate = privateHelpers.filter(
|
||||
(h) => !exportedNames.has(h.name),
|
||||
);
|
||||
|
||||
if (actualPrivate.length > MAX_HELPERS) {
|
||||
// Report on helpers beyond the threshold
|
||||
for (let i = MAX_HELPERS; i < actualPrivate.length; i++) {
|
||||
const { name, node } = actualPrivate[i];
|
||||
context.report({
|
||||
node,
|
||||
message: `Too many helper functions (${actualPrivate.length}) in component file. Move '${name}' and other helpers to utils.ts. Helpers: ${actualPrivate.map((h) => h.name).join(', ')}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
142
frontend/plugins/rules/no-multiple-components.mjs
Normal file
142
frontend/plugins/rules/no-multiple-components.mjs
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Rule: no-multiple-components
|
||||
*
|
||||
* Bans multiple React component exports in a single file.
|
||||
* Each component should have its own file.
|
||||
*
|
||||
* BAD:
|
||||
* export function ComponentA() { return <div /> }
|
||||
* export function ComponentB() { return <div /> }
|
||||
*
|
||||
* GOOD:
|
||||
* // ComponentA.tsx
|
||||
* export function ComponentA() { return <div /> }
|
||||
*
|
||||
* // ComponentB.tsx (separate file)
|
||||
* export function ComponentB() { return <div /> }
|
||||
*
|
||||
* Note: index.tsx files are exempt (re-export barrels).
|
||||
*/
|
||||
|
||||
function isPascalCase(name) {
|
||||
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
||||
}
|
||||
|
||||
function hasJSXReturn(node) {
|
||||
if (!node.body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Arrow function with direct JSX return: () => <div />
|
||||
if (node.body.type === 'JSXElement' || node.body.type === 'JSXFragment') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function body - check return statements
|
||||
if (node.body.type === 'BlockStatement') {
|
||||
for (const stmt of node.body.body) {
|
||||
if (
|
||||
stmt.type === 'ReturnStatement' &&
|
||||
stmt.argument &&
|
||||
(stmt.argument.type === 'JSXElement' ||
|
||||
stmt.argument.type === 'JSXFragment' ||
|
||||
// Conditional: condition ? <A /> : <B />
|
||||
(stmt.argument.type === 'ConditionalExpression' &&
|
||||
(stmt.argument.consequent.type === 'JSXElement' ||
|
||||
stmt.argument.alternate.type === 'JSXElement')) ||
|
||||
// Logical: condition && <A />
|
||||
(stmt.argument.type === 'LogicalExpression' &&
|
||||
(stmt.argument.left.type === 'JSXElement' ||
|
||||
stmt.argument.right.type === 'JSXElement')))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
const exportedComponents = [];
|
||||
|
||||
return {
|
||||
// export function ComponentName() { }
|
||||
ExportNamedDeclaration(node) {
|
||||
if (!node.declaration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// function ComponentName() {}
|
||||
if (node.declaration.type === 'FunctionDeclaration') {
|
||||
const name = node.declaration.id?.name;
|
||||
if (name && isPascalCase(name) && hasJSXReturn(node.declaration)) {
|
||||
exportedComponents.push({ name, node });
|
||||
}
|
||||
}
|
||||
|
||||
// const ComponentName = () => {} or const ComponentName = function() {}
|
||||
if (node.declaration.type === 'VariableDeclaration') {
|
||||
for (const declarator of node.declaration.declarations) {
|
||||
const name = declarator.id?.name;
|
||||
if (!name || !isPascalCase(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const init = declarator.init;
|
||||
if (!init) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Arrow function or function expression
|
||||
if (
|
||||
(init.type === 'ArrowFunctionExpression' ||
|
||||
init.type === 'FunctionExpression') &&
|
||||
hasJSXReturn(init)
|
||||
) {
|
||||
exportedComponents.push({ name, node });
|
||||
}
|
||||
|
||||
// React.memo(Component) or memo(Component)
|
||||
if (
|
||||
init.type === 'CallExpression' &&
|
||||
((init.callee.type === 'Identifier' &&
|
||||
(init.callee.name === 'memo' ||
|
||||
init.callee.name === 'forwardRef')) ||
|
||||
(init.callee.type === 'MemberExpression' &&
|
||||
init.callee.object.name === 'React' &&
|
||||
(init.callee.property.name === 'memo' ||
|
||||
init.callee.property.name === 'forwardRef')))
|
||||
) {
|
||||
exportedComponents.push({ name, node });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// export default function ComponentName() { }
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (node.declaration.type === 'FunctionDeclaration') {
|
||||
const name = node.declaration.id?.name;
|
||||
if (name && isPascalCase(name) && hasJSXReturn(node.declaration)) {
|
||||
exportedComponents.push({ name, node });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
if (exportedComponents.length > 1) {
|
||||
// Report on all but the first component
|
||||
for (let i = 1; i < exportedComponents.length; i++) {
|
||||
const { name, node } = exportedComponents[i];
|
||||
context.report({
|
||||
node,
|
||||
message: `Multiple component exports in same file. Move '${name}' to its own file. Found ${exportedComponents.length} components: ${exportedComponents.map((c) => c.name).join(', ')}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
39
frontend/plugins/rules/no-raw-html-text-elements.mjs
Normal file
39
frontend/plugins/rules/no-raw-html-text-elements.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Rule: no-raw-html-text-elements
|
||||
*
|
||||
* Bans raw HTML text elements (span, p, h1-h6) in JSX.
|
||||
* Use Typography component from @signozhq/ui instead.
|
||||
*
|
||||
* BAD:
|
||||
* <span>Hello</span>
|
||||
* <p>Paragraph</p>
|
||||
* <h1>Title</h1>
|
||||
*
|
||||
* GOOD:
|
||||
* <Typography>Hello</Typography>
|
||||
* <Typography.Text>Hello</Typography.Text>
|
||||
* <Typography.Title level={1}>Title</Typography.Title>
|
||||
*/
|
||||
|
||||
const BANNED_ELEMENTS = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (node.name.type !== 'JSXIdentifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementName = node.name.name;
|
||||
|
||||
if (BANNED_ELEMENTS.has(elementName)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Avoid raw '<${elementName}>' element. Use Typography component from @signozhq/ui instead.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -13,6 +13,12 @@ import noAntdComponents from './rules/no-antd-components.mjs';
|
||||
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
|
||||
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
|
||||
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
|
||||
import noGetByText from './rules/no-getByText.mjs';
|
||||
import noFireEvent from './rules/no-fireEvent.mjs';
|
||||
import noRawHtmlTextElements from './rules/no-raw-html-text-elements.mjs';
|
||||
import noMultipleComponents from './rules/no-multiple-components.mjs';
|
||||
import noInlineHelpers from './rules/no-inline-helpers.mjs';
|
||||
import maxComponentLines from './rules/max-component-lines.mjs';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
@@ -27,5 +33,11 @@ export default {
|
||||
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
|
||||
'no-css-module-bracket-access': noCssModuleBracketAccess,
|
||||
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
|
||||
'no-getByText': noGetByText,
|
||||
'no-fireEvent': noFireEvent,
|
||||
'no-raw-html-text-elements': noRawHtmlTextElements,
|
||||
'no-multiple-components': noMultipleComponents,
|
||||
'no-inline-helpers': noInlineHelpers,
|
||||
'max-component-lines': maxComponentLines,
|
||||
},
|
||||
};
|
||||
|
||||
160
frontend/pnpm-lock.yaml
generated
160
frontend/pnpm-lock.yaml
generated
@@ -19,19 +19,11 @@ overrides:
|
||||
prismjs: 1.30.0
|
||||
got: 11.8.5
|
||||
form-data: 4.0.6
|
||||
brace-expansion: ^2.0.3
|
||||
brace-expansion: ^2.0.2
|
||||
on-headers: ^1.1.0
|
||||
js-cookie: ^3.0.7
|
||||
tmp: 0.2.7
|
||||
vite: npm:rolldown-vite@7.3.1
|
||||
dompurify: 3.4.11
|
||||
js-yaml@3: 3.15.0
|
||||
js-yaml@4: 4.2.0
|
||||
yaml@1: 1.10.3
|
||||
react-router@6: 6.30.4
|
||||
markdown-it: 14.2.0
|
||||
mdast-util-to-hast@13: 13.2.1
|
||||
protocol-buffers-schema: 3.6.1
|
||||
|
||||
importers:
|
||||
|
||||
@@ -87,7 +79,7 @@ importers:
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.23
|
||||
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
|
||||
version: 0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -161,8 +153,8 @@ importers:
|
||||
specifier: ^1.10.7
|
||||
version: 1.11.20
|
||||
dompurify:
|
||||
specifier: 3.4.11
|
||||
version: 3.4.11
|
||||
specifier: 3.4.0
|
||||
version: 3.4.0
|
||||
event-source-polyfill:
|
||||
specifier: 1.0.31
|
||||
version: 1.0.31
|
||||
@@ -172,9 +164,6 @@ importers:
|
||||
history:
|
||||
specifier: 4.10.1
|
||||
version: 4.10.1
|
||||
html-to-image:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
http-status-codes:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
@@ -204,7 +193,7 @@ importers:
|
||||
version: 12.4.13(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nuqs:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
|
||||
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
overlayscrollbars:
|
||||
specifier: ^2.8.1
|
||||
version: 2.9.2
|
||||
@@ -3048,10 +3037,6 @@ packages:
|
||||
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@remix-run/router@1.23.3':
|
||||
resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -4155,8 +4140,8 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
brace-expansion@2.1.1:
|
||||
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@@ -4817,8 +4802,14 @@ packages:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.4.11:
|
||||
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
|
||||
dompurify@3.2.4:
|
||||
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
|
||||
|
||||
dompurify@3.2.7:
|
||||
resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
|
||||
|
||||
dompurify@3.4.0:
|
||||
resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==}
|
||||
|
||||
domutils@2.8.0:
|
||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||
@@ -5460,9 +5451,6 @@ packages:
|
||||
resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
|
||||
engines: {node: '>=20.10'}
|
||||
|
||||
html-to-image@1.11.13:
|
||||
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
@@ -6061,12 +6049,12 @@ packages:
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@3.15.0:
|
||||
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
|
||||
js-yaml@3.14.1:
|
||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@4.2.0:
|
||||
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@20.0.3:
|
||||
@@ -6250,8 +6238,8 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
linkify-it@5.0.2:
|
||||
resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==}
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
lint-staged@17.0.4:
|
||||
resolution: {integrity: sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA==}
|
||||
@@ -6370,8 +6358,8 @@ packages:
|
||||
mapbox-to-css-font@2.4.5:
|
||||
resolution: {integrity: sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==}
|
||||
|
||||
markdown-it@14.2.0:
|
||||
resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==}
|
||||
markdown-it@14.1.1:
|
||||
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
|
||||
hasBin: true
|
||||
|
||||
markdown-table@3.0.4:
|
||||
@@ -6435,8 +6423,8 @@ packages:
|
||||
mdast-util-to-hast@12.3.0:
|
||||
resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
|
||||
mdast-util-to-hast@13.1.0:
|
||||
resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==}
|
||||
|
||||
mdast-util-to-markdown@1.5.0:
|
||||
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
|
||||
@@ -6778,7 +6766,7 @@ packages:
|
||||
'@tanstack/react-router': ^1
|
||||
next: '>=14.2.0'
|
||||
react: '>=18.2.0 || ^19.0.0-0'
|
||||
react-router: 6.30.4
|
||||
react-router: ^5 || ^6 || ^7
|
||||
react-router-dom: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
@@ -6799,7 +6787,7 @@ packages:
|
||||
'@tanstack/react-router': ^1
|
||||
next: '>=14.2.0'
|
||||
react: '>=18.2.0 || ^19.0.0-0'
|
||||
react-router: 6.30.4
|
||||
react-router: ^5 || ^6 || ^7
|
||||
react-router-dom: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
@@ -7180,8 +7168,8 @@ packages:
|
||||
property-information@6.3.0:
|
||||
resolution: {integrity: sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==}
|
||||
|
||||
protocol-buffers-schema@3.6.1:
|
||||
resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
|
||||
protocol-buffers-schema@3.6.0:
|
||||
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
@@ -7696,8 +7684,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=15'
|
||||
|
||||
react-router@6.30.4:
|
||||
resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==}
|
||||
react-router@6.30.3:
|
||||
resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
@@ -8952,8 +8940,8 @@ packages:
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yaml@1.10.3:
|
||||
resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==}
|
||||
yaml@1.10.2:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
yaml@2.8.2:
|
||||
@@ -9074,7 +9062,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.2.0
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
@@ -10333,7 +10321,7 @@ snapshots:
|
||||
'@types/string-hash': 1.1.3
|
||||
d3-interpolate: 3.0.1
|
||||
date-fns: 4.1.0
|
||||
dompurify: 3.4.11
|
||||
dompurify: 3.2.4
|
||||
eventemitter3: 5.0.1
|
||||
fast_array_intersect: 1.1.0
|
||||
history: 4.10.1
|
||||
@@ -10484,7 +10472,7 @@ snapshots:
|
||||
camelcase: 5.3.1
|
||||
find-up: 4.1.0
|
||||
get-package-type: 0.1.0
|
||||
js-yaml: 3.15.0
|
||||
js-yaml: 3.14.1
|
||||
resolve-from: 5.0.0
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
@@ -11811,8 +11799,6 @@ snapshots:
|
||||
|
||||
'@remix-run/router@1.23.2': {}
|
||||
|
||||
'@remix-run/router@1.23.3': {}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
@@ -12050,7 +12036,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.23(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -12079,7 +12065,7 @@ snapshots:
|
||||
lodash-es: 4.18.1
|
||||
motion: 11.18.2(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nuqs: 2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
|
||||
nuqs: 2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-day-picker: 9.9.0(react@18.2.0)
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
@@ -13065,7 +13051,7 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@2.1.1:
|
||||
brace-expansion@2.0.2:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
@@ -13389,14 +13375,14 @@ snapshots:
|
||||
import-fresh: 3.3.1
|
||||
parse-json: 5.2.0
|
||||
path-type: 4.0.0
|
||||
yaml: 1.10.3
|
||||
yaml: 1.10.2
|
||||
optional: true
|
||||
|
||||
cosmiconfig@9.0.1(typescript@5.9.3):
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.2.0
|
||||
js-yaml: 4.1.1
|
||||
parse-json: 5.2.0
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
@@ -13728,7 +13714,15 @@ snapshots:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.4.11:
|
||||
dompurify@3.2.4:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dompurify@3.2.7:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dompurify@3.4.0:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
@@ -14416,7 +14410,7 @@ snapshots:
|
||||
hast-util-from-parse5: 8.0.1
|
||||
hast-util-to-parse5: 8.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.1
|
||||
mdast-util-to-hast: 13.1.0
|
||||
parse5: 7.3.0
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
@@ -14501,8 +14495,6 @@ snapshots:
|
||||
|
||||
html-tags@5.1.0: {}
|
||||
|
||||
html-to-image@1.11.13: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
http-proxy-agent@5.0.0:
|
||||
@@ -15342,12 +15334,12 @@ snapshots:
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@3.15.0:
|
||||
js-yaml@3.14.1:
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
|
||||
js-yaml@4.2.0:
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
@@ -15396,7 +15388,7 @@ snapshots:
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.24
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.2.0
|
||||
js-yaml: 4.1.1
|
||||
lodash: 4.18.1
|
||||
minimist: 1.2.8
|
||||
prettier: 3.8.3
|
||||
@@ -15533,7 +15525,7 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-it@5.0.2:
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
@@ -15654,11 +15646,11 @@ snapshots:
|
||||
|
||||
mapbox-to-css-font@2.4.5: {}
|
||||
|
||||
markdown-it@14.2.0:
|
||||
markdown-it@14.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.2
|
||||
linkify-it: 5.0.0
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
@@ -15772,7 +15764,7 @@ snapshots:
|
||||
unist-util-position: 4.0.4
|
||||
unist-util-visit: 4.1.2
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
mdast-util-to-hast@13.1.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.3
|
||||
@@ -16049,19 +16041,19 @@ snapshots:
|
||||
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.1
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.1
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.1
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.1
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
@@ -16075,7 +16067,7 @@ snapshots:
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.4.11
|
||||
dompurify: 3.2.7
|
||||
marked: 14.0.0
|
||||
|
||||
motion-dom@11.18.1:
|
||||
@@ -16216,20 +16208,20 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0):
|
||||
nuqs@2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
react-router: 6.30.4(react@18.2.0)
|
||||
react-router: 6.30.3(react@18.2.0)
|
||||
react-router-dom: 5.3.4(react@18.2.0)
|
||||
|
||||
nuqs@2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0):
|
||||
nuqs@2.8.9(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
react-router: 6.30.4(react@18.2.0)
|
||||
react-router: 6.30.3(react@18.2.0)
|
||||
react-router-dom: 5.3.4(react@18.2.0)
|
||||
|
||||
nwsapi@2.2.23: {}
|
||||
@@ -16336,7 +16328,7 @@ snapshots:
|
||||
find-up: 8.0.0
|
||||
fs-extra: 11.3.3
|
||||
jiti: 2.6.1
|
||||
js-yaml: 4.2.0
|
||||
js-yaml: 4.1.1
|
||||
remeda: 2.34.0
|
||||
string-argv: 0.3.2
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
@@ -16549,7 +16541,7 @@ snapshots:
|
||||
postcss-load-config@3.1.4(postcss@8.5.14)(ts-node@10.9.1(@types/node@16.18.25)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
lilconfig: 2.1.0
|
||||
yaml: 1.10.3
|
||||
yaml: 1.10.2
|
||||
optionalDependencies:
|
||||
postcss: 8.5.14
|
||||
ts-node: 10.9.1(@types/node@16.18.25)(typescript@5.9.3)
|
||||
@@ -16654,7 +16646,7 @@ snapshots:
|
||||
|
||||
property-information@6.3.0: {}
|
||||
|
||||
protocol-buffers-schema@3.6.1: {}
|
||||
protocol-buffers-schema@3.6.0: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
@@ -17264,7 +17256,7 @@ snapshots:
|
||||
history: 5.3.0
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-router: 6.30.4(react@18.2.0)
|
||||
react-router: 6.30.3(react@18.2.0)
|
||||
react-router-dom: 5.3.4(react@18.2.0)
|
||||
|
||||
react-router-dom@5.3.4(react@18.2.0):
|
||||
@@ -17291,9 +17283,9 @@ snapshots:
|
||||
tiny-invariant: 1.3.3
|
||||
tiny-warning: 1.0.3
|
||||
|
||||
react-router@6.30.4(react@18.2.0):
|
||||
react-router@6.30.3(react@18.2.0):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.23.3
|
||||
'@remix-run/router': 1.23.2
|
||||
react: 18.2.0
|
||||
|
||||
react-style-singleton@2.2.3(@types/react@18.0.26)(react@18.2.0):
|
||||
@@ -17517,7 +17509,7 @@ snapshots:
|
||||
|
||||
resolve-protobuf-schema@2.1.0:
|
||||
dependencies:
|
||||
protocol-buffers-schema: 3.6.1
|
||||
protocol-buffers-schema: 3.6.0
|
||||
|
||||
resolve@1.22.11:
|
||||
dependencies:
|
||||
@@ -18209,7 +18201,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@gerrit0/mini-shiki': 3.23.0
|
||||
lunr: 2.3.9
|
||||
markdown-it: 14.2.0
|
||||
markdown-it: 14.1.1
|
||||
minimatch: 10.2.5
|
||||
typescript: 5.9.3
|
||||
yaml: 2.8.4
|
||||
@@ -18673,7 +18665,7 @@ snapshots:
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yaml@1.10.3: {}
|
||||
yaml@1.10.2: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
|
||||
@@ -62,6 +62,6 @@
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
|
||||
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
|
||||
}
|
||||
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
|
||||
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
|
||||
}
|
||||
|
||||
@@ -87,6 +87,6 @@
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
|
||||
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
|
||||
}
|
||||
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
|
||||
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
|
||||
}
|
||||
|
||||
@@ -329,3 +329,10 @@ export const LLMObservabilityPage = Loadable(
|
||||
/* webpackChunkName: "LLM Observability Page" */ 'pages/LLMObservability'
|
||||
),
|
||||
);
|
||||
|
||||
export const LLMObservabilityModelPricingPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "LLM Observability Model Pricing Page" */ 'pages/LLMObservabilityModelPricing'
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LLMObservabilityPage,
|
||||
LLMObservabilityModelPricingPage,
|
||||
LiveLogs,
|
||||
Login,
|
||||
Logs,
|
||||
@@ -514,24 +515,17 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
path: ROUTES.LLM_OBSERVABILITY_BASE,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'LLM_OBSERVABILITY_ATTRIBUTE_MAPPING',
|
||||
key: 'LLM_OBSERVABILITY_BASE',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
path: ROUTES.LLM_OBSERVABILITY_MODEL_PRICING,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'LLM_OBSERVABILITY_OVERVIEW',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'LLM_OBSERVABILITY_CONFIGURATION',
|
||||
component: LLMObservabilityModelPricingPage,
|
||||
key: 'LLM_OBSERVABILITY_MODEL_PRICING',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -269,170 +269,3 @@ export function mockQueryRangeV5WithEventsResponse({
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export type MockTracesOptions = {
|
||||
offset?: number;
|
||||
pageSize?: number;
|
||||
hasMore?: boolean;
|
||||
delay?: number;
|
||||
customTraces?: Array<{
|
||||
serviceName?: string;
|
||||
name?: string;
|
||||
durationNano?: number;
|
||||
httpMethod?: string;
|
||||
responseStatusCode?: string;
|
||||
}>;
|
||||
onReceiveRequest?: (
|
||||
req: RestRequest,
|
||||
) =>
|
||||
| undefined
|
||||
| void
|
||||
| Omit<MockTracesOptions, 'onReceiveRequest'>
|
||||
| Promise<Omit<MockTracesOptions, 'onReceiveRequest'>>
|
||||
| Promise<void>;
|
||||
};
|
||||
|
||||
const createTracesResponse = ({
|
||||
offset = 0,
|
||||
pageSize = 10,
|
||||
hasMore = true,
|
||||
customTraces,
|
||||
}: MockTracesOptions): MetricRangePayloadV5 => {
|
||||
const serviceNames = ['frontend', 'backend', 'database', 'api-gateway'];
|
||||
const spanNames = [
|
||||
'GET /api/users',
|
||||
'POST /api/orders',
|
||||
'SELECT * FROM users',
|
||||
];
|
||||
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE'];
|
||||
const statusCodes = ['200', '201', '400', '404', '500'];
|
||||
|
||||
const rows = customTraces
|
||||
? customTraces.map((trace, index) => {
|
||||
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
|
||||
const currentTimestamp = new Date(baseTimestamp - index * 60000);
|
||||
return {
|
||||
timestamp: currentTimestamp.toISOString(),
|
||||
data: {
|
||||
serviceName:
|
||||
trace.serviceName ?? serviceNames[index % serviceNames.length],
|
||||
name: trace.name ?? spanNames[index % spanNames.length],
|
||||
durationNano: trace.durationNano ?? 1000000 + index * 100000,
|
||||
httpMethod: trace.httpMethod ?? httpMethods[index % httpMethods.length],
|
||||
responseStatusCode:
|
||||
trace.responseStatusCode ?? statusCodes[index % statusCodes.length],
|
||||
traceID: `trace-id-${index}`,
|
||||
spanID: `span-id-${index}`,
|
||||
},
|
||||
};
|
||||
})
|
||||
: Array.from(
|
||||
{ length: hasMore ? pageSize : Math.ceil(pageSize / 2) },
|
||||
(_, index) => {
|
||||
const cumulativeIndex = offset + index;
|
||||
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
|
||||
const currentTimestamp = new Date(baseTimestamp - cumulativeIndex * 60000);
|
||||
return {
|
||||
timestamp: currentTimestamp.toISOString(),
|
||||
data: {
|
||||
serviceName: serviceNames[cumulativeIndex % serviceNames.length],
|
||||
name: spanNames[cumulativeIndex % spanNames.length],
|
||||
durationNano: 1000000 + cumulativeIndex * 100000,
|
||||
httpMethod: httpMethods[cumulativeIndex % httpMethods.length],
|
||||
responseStatusCode: statusCodes[cumulativeIndex % statusCodes.length],
|
||||
traceID: `trace-id-${cumulativeIndex}`,
|
||||
spanID: `span-id-${cumulativeIndex}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
data: {
|
||||
type: 'raw',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName: 'A',
|
||||
nextCursor: hasMore ? 'next-cursor-token' : '',
|
||||
rows,
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
bytesScanned: 9682976,
|
||||
durationMs: 295,
|
||||
rowsScanned: 34198,
|
||||
stepIntervals: { A: 170 },
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function mockQueryRangeV5WithTracesResponse({
|
||||
hasMore = true,
|
||||
offset = 0,
|
||||
pageSize = 10,
|
||||
delay = 0,
|
||||
customTraces,
|
||||
onReceiveRequest,
|
||||
}: MockTracesOptions = {}): void {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) =>
|
||||
res(
|
||||
...(delay ? [ctx.delay(delay)] : []),
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
createTracesResponse(
|
||||
(await onReceiveRequest?.(req)) ?? {
|
||||
hasMore,
|
||||
pageSize,
|
||||
offset,
|
||||
customTraces,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function mockQueryRangeV5WithEmptyTraces(): void {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: {
|
||||
type: 'raw',
|
||||
data: {
|
||||
results: [{ queryName: 'A', nextCursor: '', rows: [] }],
|
||||
},
|
||||
meta: {
|
||||
bytesScanned: 0,
|
||||
durationMs: 10,
|
||||
rowsScanned: 0,
|
||||
stepIntervals: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function mockQueryRangeV5WithKeyNotFoundError(): void {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'invalid_input',
|
||||
errors: [{ message: 'key not found' }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2813,7 +2813,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
cosmosdb = 'cosmosdb',
|
||||
cassandradb = 'cassandradb',
|
||||
redis = 'redis',
|
||||
cloudsql_postgres = 'cloudsql_postgres',
|
||||
cloudsql = 'cloudsql',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -5005,10 +5005,6 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -5084,10 +5080,6 @@ export interface DashboardtypesListedDashboardV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -5585,7 +5577,6 @@ export enum InframonitoringtypesCheckTypeDTO {
|
||||
namespaces = 'namespaces',
|
||||
clusters = 'clusters',
|
||||
volumes = 'volumes',
|
||||
kube_containers = 'kube_containers',
|
||||
}
|
||||
export interface InframonitoringtypesMissingMetricsComponentEntryDTO {
|
||||
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
|
||||
@@ -5865,174 +5856,6 @@ export interface InframonitoringtypesClustersDTO {
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesContainerCountsByReadyDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
notReady: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
ready: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesContainerCountsByStatusDTO {
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
completed: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
containerCannotRun: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
containerCreating: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
crashLoopBackOff: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
createContainerConfigError: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
errImagePull: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
error: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
imagePullBackOff: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
oomKilled: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
running: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
terminated: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
unknown: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
waiting: number;
|
||||
}
|
||||
|
||||
export enum InframonitoringtypesContainerReadyDTO {
|
||||
ready = 'ready',
|
||||
not_ready = 'not_ready',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type InframonitoringtypesContainerRecordDTOMeta =
|
||||
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
|
||||
|
||||
export enum InframonitoringtypesContainerStatusDTO {
|
||||
running = 'running',
|
||||
waiting = 'waiting',
|
||||
terminated = 'terminated',
|
||||
crashloopbackoff = 'crashloopbackoff',
|
||||
imagepullbackoff = 'imagepullbackoff',
|
||||
errimagepull = 'errimagepull',
|
||||
createcontainerconfigerror = 'createcontainerconfigerror',
|
||||
containercreating = 'containercreating',
|
||||
oomkilled = 'oomkilled',
|
||||
completed = 'completed',
|
||||
error = 'error',
|
||||
containercannotrun = 'containercannotrun',
|
||||
unknown = 'unknown',
|
||||
no_data = 'no_data',
|
||||
}
|
||||
export interface InframonitoringtypesContainerRecordDTO {
|
||||
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
|
||||
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
containerName: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
cpu: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
cpuLimitUtilization: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
cpuRequestUtilization: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
memory: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
memoryLimitUtilization: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
memoryRequestUtilization: number;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesContainerRecordDTOMeta;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
podUID: string;
|
||||
ready: InframonitoringtypesContainerReadyDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
restarts: number;
|
||||
status: InframonitoringtypesContainerStatusDTO;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesContainersDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
endTimeBeforeRetention: boolean;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
records: InframonitoringtypesContainerRecordDTO[];
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
type: InframonitoringtypesResponseTypeDTO;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO;
|
||||
}
|
||||
|
||||
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6090,16 +5913,8 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
misscheduledNodes: number;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
readyNodes: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetsDTO {
|
||||
@@ -6623,33 +6438,6 @@ export interface InframonitoringtypesPostableClustersDTO {
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableContainersDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
end: number;
|
||||
filter?: Querybuildertypesv5FilterDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
offset?: number;
|
||||
orderBy?: Querybuildertypesv5OrderByDTO;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
start: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesPostableDaemonSetsDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -8496,12 +8284,10 @@ export interface RuletypesBasicRuleThresholdDTO {
|
||||
op: RuletypesCompareOperatorDTO;
|
||||
/**
|
||||
* @type number,null
|
||||
* @format double
|
||||
*/
|
||||
recoveryTarget?: number | null;
|
||||
/**
|
||||
* @type number,null
|
||||
* @format double
|
||||
*/
|
||||
target: number | null;
|
||||
/**
|
||||
@@ -8695,7 +8481,6 @@ export interface RuletypesRuleConditionDTO {
|
||||
selectedQueryName?: string;
|
||||
/**
|
||||
* @type number,null
|
||||
* @format double
|
||||
*/
|
||||
target?: number | null;
|
||||
/**
|
||||
@@ -9194,76 +8979,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperConfigDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
sources: SpantypesSpanMapperSourceDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
fieldContext: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
groupId: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMappersDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
items: SpantypesSpanMapperDTO[];
|
||||
}
|
||||
|
||||
export enum SpantypesSpanAggregationTypeDTO {
|
||||
span_count = 'span_count',
|
||||
execution_time_percentage = 'execution_time_percentage',
|
||||
@@ -9510,6 +9225,30 @@ export interface SpantypesPostableFlamegraphDTO {
|
||||
selectedSpanId?: string;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperConfigDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
sources: SpantypesSpanMapperSourceDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
@@ -9558,6 +9297,45 @@ export interface SpantypesPostableWaterfallDTO {
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
fieldContext: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
group_id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesUpdatableSpanMapperDTO {
|
||||
config?: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
@@ -10859,7 +10637,7 @@ export type ListSpanMappersPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type ListSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMappersDTO;
|
||||
data: SpantypesGettableSpanMapperGroupsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -11263,14 +11041,6 @@ export type ListJobs200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListContainers200 = {
|
||||
data: InframonitoringtypesContainersDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListNamespaces200 = {
|
||||
data: InframonitoringtypesNamespacesDTO;
|
||||
/**
|
||||
@@ -11596,19 +11366,6 @@ export type GetPublicDashboardPanelQueryRangeV2PathParameters = {
|
||||
id: string;
|
||||
key: string;
|
||||
};
|
||||
export type GetPublicDashboardPanelQueryRangeV2Params = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
startTime?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
export type GetPublicDashboardPanelQueryRangeV2200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Popover, Tooltip } from 'antd';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { useExportRawData } from 'hooks/useExportData/useServerExport';
|
||||
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
|
||||
import { Download, LoaderCircle } from '@signozhq/icons';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ type BadgeColor =
|
||||
|
||||
interface HttpStatusBadgeProps {
|
||||
statusCode: string | number;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function getStatusCodeColor(statusCode: number): BadgeColor {
|
||||
@@ -37,8 +35,6 @@ function getStatusCodeColor(statusCode: number): BadgeColor {
|
||||
|
||||
function HttpStatusBadge({
|
||||
statusCode,
|
||||
testId,
|
||||
className,
|
||||
}: HttpStatusBadgeProps): JSX.Element | null {
|
||||
const numericStatusCode = Number(statusCode);
|
||||
|
||||
@@ -49,12 +45,7 @@ function HttpStatusBadge({
|
||||
const color = getStatusCodeColor(numericStatusCode);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color={color}
|
||||
variant="outline"
|
||||
data-testid={testId}
|
||||
className={className}
|
||||
>
|
||||
<Badge color={color} variant="outline">
|
||||
{statusCode}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
padding: 0px 8px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
|
||||
background: var(--input-with-label-background-color, var(--l2-background));
|
||||
color: var(--input-with-label-color, var(--l2-foreground));
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
color: var(--l2-foreground);
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
@@ -35,54 +35,21 @@
|
||||
min-width: 150px;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
--input-border-radius: 0px;
|
||||
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
|
||||
background: var(--input-with-label-background-color, var(--l2-background));
|
||||
color: var(--input-with-label-color, var(--l2-foreground));
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
border-right: none;
|
||||
border-left: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
font-size: 12px !important;
|
||||
line-height: 25px;
|
||||
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.ant-select:hover .ant-select-selector,
|
||||
.ant-select-focused .ant-select-selector {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.input__has-label-after {
|
||||
margin-left: -1px;
|
||||
|
||||
.ant-select-selector {
|
||||
margin-left: -1px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.input__has-close-button {
|
||||
.ant-select-selector {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
~ .close-btn {
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
line-height: 27px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--input-with-label-color, var(--l3-foreground)) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
@@ -96,35 +63,25 @@
|
||||
|
||||
.close-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
|
||||
background: var(--input-with-label-background-color, var(--l2-background));
|
||||
height: 100%;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&.labelAfter {
|
||||
.input {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
|
||||
background: var(--input-with-label-background-color, var(--l2-background));
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
border-left: none;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,7 @@ function InputWithLabel({
|
||||
>
|
||||
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
<Input
|
||||
className={cx('input', {
|
||||
'input__has-label-after': !labelAfter,
|
||||
'input__has-close-button': !!onClose,
|
||||
})}
|
||||
className="input"
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={inputValue}
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--query-builder-v2-border-color, var(--l2-border)),
|
||||
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
|
||||
var(--l1-border),
|
||||
var(--l1-border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -101,8 +101,7 @@
|
||||
top: 12px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-left: 6px dotted
|
||||
var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-left: 6px dotted var(--l1-border);
|
||||
}
|
||||
|
||||
/* Horizontal line pointing from vertical to the item */
|
||||
@@ -115,8 +114,8 @@
|
||||
height: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--query-builder-v2-border-color, var(--l2-border)),
|
||||
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
|
||||
var(--l1-border),
|
||||
var(--l1-border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -242,8 +241,7 @@
|
||||
top: 12px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-left: 6px dotted
|
||||
var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-left: 6px dotted var(--l1-border);
|
||||
}
|
||||
|
||||
/* Horizontal line pointing from vertical to the item */
|
||||
@@ -256,8 +254,8 @@
|
||||
height: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--query-builder-v2-border-color, var(--l2-border)),
|
||||
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
|
||||
var(--l1-border),
|
||||
var(--l1-border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -275,16 +273,6 @@
|
||||
line-height: 16px; /* 128.571% */
|
||||
|
||||
resize: none;
|
||||
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
);
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
|
||||
&:placeholder {
|
||||
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
.formula-legend {
|
||||
@@ -294,42 +282,15 @@
|
||||
.ant-input-group-addon {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
);
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
background: var(--l2-background);
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border-top-left-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
|
||||
height: 36px;
|
||||
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
);
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
|
||||
position: relative;
|
||||
margin-left: -1px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
z-index: 1;
|
||||
border-color: var(--internal-ant-border-color-hover);
|
||||
}
|
||||
|
||||
&:placeholder {
|
||||
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,8 +323,8 @@
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--query-builder-v2-border-color, var(--l2-border)),
|
||||
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
|
||||
var(--l1-border),
|
||||
var(--l1-border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -434,8 +395,8 @@
|
||||
width: 1px;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--query-builder-v2-border-color, var(--l2-border)),
|
||||
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
|
||||
var(--l1-border),
|
||||
var(--l1-border) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
@@ -451,7 +412,7 @@
|
||||
min-width: 120px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@@ -496,16 +457,13 @@
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
|
||||
background: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
height: 36px !important;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background: var(--l1-background) !important;
|
||||
height: 34px !important;
|
||||
box-sizing: border-box !important;
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--query-builder-v2-color, var(--l1-foreground));
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,11 +92,6 @@
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selector {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
min-width: max-content !important;
|
||||
max-width: 100% !important;
|
||||
@@ -105,12 +100,9 @@
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
border: 1.005px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
@@ -131,7 +123,6 @@
|
||||
.input {
|
||||
flex: initial;
|
||||
width: 100px !important;
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
font-size: 12px !important;
|
||||
line-height: 25px;
|
||||
line-height: 27px;
|
||||
&::placeholder {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,9 @@
|
||||
.ant-select-selector {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
@@ -36,49 +33,36 @@
|
||||
min-height: 36px;
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(
|
||||
--query-builder-v2-placeholder-color,
|
||||
var(--l3-foreground)
|
||||
) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qb-select-popover.ant-select-dropdown {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background: var(--query-builder-v2-background-color, var(--l2-background));
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
.ant-select-dropdown {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.ant-select-item {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
.ant-select-item {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
&:hover,
|
||||
&.ant-select-item-option-active {
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.ant-select-item-option-active {
|
||||
background: var(
|
||||
--query-builder-v2-selected-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
}
|
||||
|
||||
&.ant-select-item-option-selected {
|
||||
background: var(
|
||||
--query-builder-v2-selected-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
font-weight: 600;
|
||||
&.ant-select-item-option-selected {
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l1-border);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ export const MetricsSelect = memo(function MetricsSelect({
|
||||
{signalSourceChangeEnabled && (
|
||||
<Select
|
||||
className="source-selector"
|
||||
popupClassName="qb-select-popover"
|
||||
placeholder="Source"
|
||||
options={SOURCE_OPTIONS}
|
||||
value={source}
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
|
||||
|
||||
.query-add-ons {
|
||||
width: 100%;
|
||||
|
||||
--toggle-group-secondary-bg: var(
|
||||
--query-builder-v2-toggle-group-background-color,
|
||||
var(--l1-background-hover)
|
||||
);
|
||||
--toggle-group-secondary-border: var(
|
||||
--query-builder-v2-toggle-group-border-color,
|
||||
var(--l2-border)
|
||||
);
|
||||
--toggle-group-secondary-active-bg: var(
|
||||
--query-builder-v2-toggle-group-active-background-color,
|
||||
var(--l1-background)
|
||||
);
|
||||
--toggle-group-secondary-bg-hover: var(
|
||||
--query-builder-v2-toggle-group-background-color-hover,
|
||||
var(--l2-background)
|
||||
);
|
||||
|
||||
.add-on-tab-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -44,33 +29,32 @@
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
> button {
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
border-left: none;
|
||||
min-width: 120px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid
|
||||
var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-left: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
&[data-state='on'] {
|
||||
color: var(--text-robin-500);
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
display: none;
|
||||
|
||||
&::before {
|
||||
background: var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background: var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +65,7 @@
|
||||
height: 30px;
|
||||
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@@ -94,13 +78,10 @@
|
||||
align-items: center;
|
||||
|
||||
.having-filter-select-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
background: var(--query-builder-v2-background-color, var(--l2-background));
|
||||
padding-right: 38px;
|
||||
align-items: center;
|
||||
|
||||
.having-filter-select-editor {
|
||||
border-radius: 2px;
|
||||
@@ -125,17 +106,15 @@
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-left-width: 0px;
|
||||
border-right-width: 0px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
padding: 0px !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,32 +218,17 @@
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
min-height: 34px;
|
||||
line-height: 32px !important;
|
||||
line-height: 36px !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
|
||||
&,
|
||||
.ͼ1a {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
}
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
@@ -273,11 +237,8 @@
|
||||
}
|
||||
|
||||
.chip-decorator {
|
||||
background: var(
|
||||
--query-builder-v2-chip-decorator-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
|
||||
background: var(--l3-background) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
margin-right: 4px;
|
||||
@@ -285,38 +246,34 @@
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cm-activeLine > span {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.cm-placeholder {
|
||||
color: var(
|
||||
--query-builder-v2-placeholder-color,
|
||||
var(--l3-foreground)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background: var(--query-builder-v2-background-color, var(--l2-background));
|
||||
height: 100%;
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l2-background);
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
|
||||
border-left: transparent;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
|
||||
&:focus:not(:focus-visible),
|
||||
&.ant-btn:focus:not(:focus-visible) {
|
||||
border-color: var(--l2-border);
|
||||
border-left-color: transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,8 +300,20 @@
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
input {
|
||||
min-height: 36px;
|
||||
$add-on-row-height: 38px;
|
||||
|
||||
.periscope-input-with-label {
|
||||
.input {
|
||||
.ant-select {
|
||||
height: $add-on-row-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-label {
|
||||
.input {
|
||||
height: $add-on-row-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
|
||||
&.error {
|
||||
.cm-editor {
|
||||
@@ -51,15 +51,14 @@
|
||||
|
||||
.cm-content {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
padding: 0px !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +74,7 @@
|
||||
right: 0px !important;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
@@ -119,7 +118,7 @@
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
.cm-completionIcon {
|
||||
@@ -128,10 +127,7 @@
|
||||
|
||||
&:hover,
|
||||
&[aria-selected='true'] {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
@@ -146,24 +142,15 @@
|
||||
.cm-line {
|
||||
line-height: 36px !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
@@ -172,11 +159,8 @@
|
||||
}
|
||||
|
||||
.chip-decorator {
|
||||
background: var(
|
||||
--query-builder-v2-chip-decorator-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
color: var(--query-builder-v2-color, var(--l1-foreground)) !important;
|
||||
background: var(--l3-background) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
margin-right: 4px;
|
||||
@@ -184,10 +168,7 @@
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
@@ -220,11 +201,12 @@
|
||||
|
||||
.close-btn {
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background: var(--query-builder-v2-background-color, var(--l2-background));
|
||||
height: 100%;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
|
||||
border-left: transparent;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
@@ -235,13 +217,13 @@
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
color: var(--l1-foreground);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@@ -256,10 +238,9 @@
|
||||
.query-aggregation-interval-input-container {
|
||||
.query-aggregation-interval-input {
|
||||
input {
|
||||
min-height: 36px;
|
||||
max-width: 120px;
|
||||
&::placeholder {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,8 +251,8 @@
|
||||
|
||||
.query-aggregation-error-popover {
|
||||
.ant-popover-inner {
|
||||
background-color: var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background-color: var(--l1-border);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.add-trace-operator-button,
|
||||
.add-new-query-button,
|
||||
.add-formula-button {
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
background: var(--query-builder-v2-background-color, var(--l2-background));
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -40,14 +40,11 @@ $max-recents-shown: 5;
|
||||
.query-status-container {
|
||||
width: 32px;
|
||||
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
@@ -86,16 +83,16 @@ $max-recents-shown: 5;
|
||||
|
||||
.cm-content {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border: 1px solid var(--l1-border);
|
||||
padding: 0px !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--query-builder-v2-border-color, var(--l2-border));
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
outline: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
|
||||
outline: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
@@ -186,17 +183,11 @@ $max-recents-shown: 5;
|
||||
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
color: var(--l2-foreground) !important;
|
||||
|
||||
&:hover {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@@ -214,10 +205,7 @@ $max-recents-shown: 5;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
@@ -286,49 +274,25 @@ $max-recents-shown: 5;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
line-height: 36px !important;
|
||||
line-height: 34px !important;
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
background-color: var(
|
||||
--query-builder-v2-background-color,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
|
||||
&,
|
||||
.ͼ1a {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
}
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
::-moz-selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
var(--l3-background)
|
||||
) !important;
|
||||
background: var(--l3-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cm-placeholder {
|
||||
color: var(
|
||||
--query-builder-v2-placeholder-color,
|
||||
var(--l3-foreground)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-position {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -65,14 +65,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
}
|
||||
|
||||
&-aggregation-container {
|
||||
|
||||
@@ -47,7 +47,7 @@ function CheckboxValueRow({
|
||||
{customRendererForValue ? (
|
||||
customRendererForValue(value)
|
||||
) : (
|
||||
<TooltipSimple title={String(value)} side="top" align="start">
|
||||
<TooltipSimple title={value} side="top" align="center" arrow>
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -151,7 +151,6 @@ function AddKeyModal(): JSX.Element {
|
||||
className="add-key-modal"
|
||||
showCloseButton
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
testId="add-key-modal"
|
||||
>
|
||||
{phase === Phase.FORM && (
|
||||
<KeyFormPhase
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -175,7 +175,6 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
|
||||
}
|
||||
showCloseButton={!isRevokeConfirmOpen}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
testId="edit-key-modal"
|
||||
footer={
|
||||
isRevokeConfirmOpen ? (
|
||||
<RevokeKeyFooter
|
||||
|
||||
@@ -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]} — {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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]} — {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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -57,13 +57,10 @@ function TimelineV3(props: ITimelineV3Props): JSX.Element {
|
||||
}
|
||||
|
||||
const timeAtCursor = offsetTimestamp + cursorXPercent * spread;
|
||||
// Use the same width-derived interval spread the ticks use, so the badge
|
||||
// unit always matches the tick unit (narrow rulers pick fewer intervals).
|
||||
const intervalSpread = spread / getMinimumIntervalsBasedOnWidth(width);
|
||||
const unit = getIntervalUnit(intervalSpread, offsetTimestamp);
|
||||
const unit = getIntervalUnit(spread, offsetTimestamp);
|
||||
const formatted = toFixed(resolveTimeFromInterval(timeAtCursor, unit), 2);
|
||||
return `${formatted}${unit.name}`;
|
||||
}, [cursorXPercent, spread, offsetTimestamp, width]);
|
||||
}, [cursorXPercent, spread, offsetTimestamp]);
|
||||
|
||||
if (endTimestamp < startTimestamp) {
|
||||
console.error(
|
||||
@@ -97,17 +94,12 @@ function TimelineV3(props: ITimelineV3Props): JSX.Element {
|
||||
>
|
||||
<text
|
||||
x={index === intervals.length - 1 ? -10 : 0}
|
||||
y={timelineHeight * 2 - 3}
|
||||
y={timelineHeight * 2}
|
||||
fill={strokeColor}
|
||||
>
|
||||
{interval.label}
|
||||
</text>
|
||||
<line
|
||||
y1={0}
|
||||
y2={timelineHeight - 3}
|
||||
stroke={strokeColor}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line y1={0} y2={timelineHeight} stroke={strokeColor} strokeWidth="1" />
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import {
|
||||
getIntervals,
|
||||
getIntervalUnit,
|
||||
getMinimumIntervalsBasedOnWidth,
|
||||
} from '../utils';
|
||||
|
||||
// A tick label looks like "200.00ms" / "1.10s" — grab the trailing unit name.
|
||||
function unitOfLabel(label: string): string {
|
||||
const match = label.match(/[a-z]+$/i);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
describe('getMinimumIntervalsBasedOnWidth', () => {
|
||||
it('returns fewer intervals for narrower rulers', () => {
|
||||
expect(getMinimumIntervalsBasedOnWidth(500)).toBe(3);
|
||||
expect(getMinimumIntervalsBasedOnWidth(700)).toBe(4);
|
||||
expect(getMinimumIntervalsBasedOnWidth(900)).toBe(5);
|
||||
expect(getMinimumIntervalsBasedOnWidth(1200)).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIntervalUnit', () => {
|
||||
it('selects the unit from the interval spread', () => {
|
||||
expect(getIntervalUnit(130, 0).name).toBe('ms');
|
||||
expect(getIntervalUnit(1100, 0).name).toBe('s');
|
||||
expect(getIntervalUnit(70_000, 0).name).toBe('m');
|
||||
});
|
||||
|
||||
it('accounts for a large offset (deep-zoom labels stay readable)', () => {
|
||||
// Cursor spread is tiny but the window starts 5,000,000ms into the trace.
|
||||
expect(getIntervalUnit(100, 5_000_000).name).toBe('hr');
|
||||
});
|
||||
|
||||
// Regression: the interval COUNT changes the chosen unit, so the crosshair
|
||||
// badge must use the same width-derived count as the ticks. On a 5.5s trace a
|
||||
// narrow ruler (5 intervals → 1100ms → "s") and a wide one (6 intervals →
|
||||
// 916ms → "ms") pick different units.
|
||||
it('can resolve to different units for the same spread at different counts', () => {
|
||||
const spread = 5500;
|
||||
expect(getIntervalUnit(spread / 5, 0).name).toBe('s');
|
||||
expect(getIntervalUnit(spread / 6, 0).name).toBe('ms');
|
||||
});
|
||||
});
|
||||
|
||||
describe('badge/tick unit consistency', () => {
|
||||
// The invariant the fix guarantees: when the badge and the ticks are fed the
|
||||
// same width-derived intervalSpread, every tick label uses the badge's unit.
|
||||
it.each([
|
||||
{ spread: 1287, width: 900 },
|
||||
{ spread: 5500, width: 900 }, // narrow → 5 intervals, the mismatch case
|
||||
{ spread: 5500, width: 1200 }, // wide → 6 intervals
|
||||
{ spread: 120_000, width: 700 },
|
||||
])('spread=$spread width=$width', ({ spread, width }) => {
|
||||
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
|
||||
const intervalSpread = spread / minIntervals;
|
||||
const badgeUnit = getIntervalUnit(intervalSpread, 0).name;
|
||||
const intervals = getIntervals(intervalSpread, spread, 0);
|
||||
|
||||
intervals.forEach((interval) => {
|
||||
expect(unitOfLabel(interval.label)).toBe(badgeUnit);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,18 +10,14 @@ export type { Interval };
|
||||
|
||||
/**
|
||||
* Select the interval unit matching the timeline's logic.
|
||||
* Exported so crosshair labels use the same unit as the timeline ticks.
|
||||
*
|
||||
* Takes the already-computed `intervalSpread` (spread / minIntervals) rather
|
||||
* than deriving it from a hardcoded interval count — the tick count is
|
||||
* width-dependent (`getMinimumIntervalsBasedOnWidth`), so callers must pass the
|
||||
* same `intervalSpread` the ticks use or the badge unit can diverge from the
|
||||
* ticks (e.g. ms vs s) on narrower rulers like the waterfall.
|
||||
* Exported so crosshair labels use the same unit as timeline ticks.
|
||||
*/
|
||||
export function getIntervalUnit(
|
||||
intervalSpread: number,
|
||||
spread: number,
|
||||
offsetTimestamp: number,
|
||||
): IIntervalUnit {
|
||||
const minIntervals = 6;
|
||||
const intervalSpread = spread / minIntervals;
|
||||
const valueForUnitSelection = Math.max(offsetTimestamp, intervalSpread);
|
||||
let unit: IIntervalUnit = INTERVAL_UNITS[0];
|
||||
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* Overlay stays below content */
|
||||
[data-slot='dialog-overlay'] {
|
||||
z-index: 1000 !important;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Dialog content always above overlay */
|
||||
[data-slot='dialog-content'] {
|
||||
position: fixed;
|
||||
z-index: 1001 !important;
|
||||
z-index: 60;
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ export enum FeatureKeys {
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
USE_DASHBOARD_V2 = 'use_dashboard_v2',
|
||||
USE_INFRA_MONITORING_V2 = 'use_infra_monitoring_v2',
|
||||
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
|
||||
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum LOCALSTORAGE {
|
||||
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -89,10 +89,8 @@ const ROUTES = {
|
||||
AI_ASSISTANT_BASE: '/ai-assistant',
|
||||
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
|
||||
MCP_SERVER: '/settings/mcp-server',
|
||||
LLM_OBSERVABILITY_ATTRIBUTE_MAPPING: '/llm-observability/attribute-mapping',
|
||||
LLM_OBSERVABILITY_BASE: '/llm-observability',
|
||||
LLM_OBSERVABILITY_OVERVIEW: '/llm-observability/overview',
|
||||
LLM_OBSERVABILITY_CONFIGURATION: '/llm-observability/configuration',
|
||||
LLM_OBSERVABILITY_MODEL_PRICING: '/llm-observability/settings/model-pricing',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export enum SESSIONSTORAGE {
|
||||
RETRY_LAZY_REFRESHED = 'retry-lazy-refreshed',
|
||||
VIEW_PANEL_HANDOFF = 'view-panel-handoff',
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -41,28 +41,6 @@
|
||||
|
||||
.steps-container {
|
||||
width: 80%;
|
||||
|
||||
.alert-query-section-container {
|
||||
--query-builder-v2-color: var(--l2-foreground);
|
||||
--query-builder-v2-background-color: var(--l3-background);
|
||||
--query-builder-v2-border-color: var(--l3-border);
|
||||
--query-builder-v2-selection-background-color: var(--l2-background);
|
||||
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
|
||||
--query-builder-v2-placeholder-color: var(--l3-foreground);
|
||||
--query-builder-v2-selected-background-color: var(--l3-foreground);
|
||||
--query-search-background-color: var(--l3-background);
|
||||
--query-search-background-color-selection: var(--l3-background);
|
||||
--input-with-label-border-color: var(--l3-border);
|
||||
--input-with-label-background-color: var(--l3-background);
|
||||
--input-with-label-color: var(--l2-foreground);
|
||||
--query-builder-v2-toggle-group-background-color: var(--l3-background);
|
||||
--query-builder-v2-toggle-group-border-color: var(--l3-border);
|
||||
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
|
||||
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
|
||||
--input-height: 36px;
|
||||
--input-hover-background: var(--l3-background);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.qb-chart-preview-container {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.wrapper :global(button[data-color]) {
|
||||
--checkbox-checked-background: var(--series-color);
|
||||
--checkbox-border-color: var(--series-color);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -3,30 +3,6 @@
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
--query-builder-v2-color: var(--l2-foreground);
|
||||
--query-builder-v2-background-color: var(--l3-background);
|
||||
--query-builder-v2-border-color: var(--l3-border);
|
||||
--query-builder-v2-selection-background-color: var(--l2-background);
|
||||
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
|
||||
--query-builder-v2-placeholder-color: var(--l3-foreground);
|
||||
--query-builder-v2-selected-background-color: var(--l3-foreground);
|
||||
--query-search-background-color: var(--l3-background);
|
||||
--query-search-background-color-selection: var(--l3-background);
|
||||
--input-with-label-border-color: var(--l3-border);
|
||||
--input-with-label-background-color: var(--l3-background);
|
||||
--input-with-label-color: var(--l2-foreground);
|
||||
--query-builder-v2-toggle-group-background-color: var(--l3-background);
|
||||
--query-builder-v2-toggle-group-border-color: var(--l3-border);
|
||||
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
|
||||
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
|
||||
--input-height: 36px;
|
||||
--input-hover-background: var(--l3-background);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-background: var(--l3-background);
|
||||
--input-focus-background: var(--l3-background);
|
||||
--input-border-color: var(--l3-border);
|
||||
|
||||
.full-view-header-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -66,7 +66,6 @@ function WidgetGraphComponent({
|
||||
customOnRowClick,
|
||||
customTimeRangeWindowForCoRelation,
|
||||
enableDrillDown,
|
||||
hidePagination,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@@ -431,7 +430,6 @@ function WidgetGraphComponent({
|
||||
customSeries={customSeries}
|
||||
customOnRowClick={customOnRowClick}
|
||||
enableDrillDown={enableDrillDown}
|
||||
hidePagination={hidePagination}
|
||||
onColumnWidthsChange={onColumnWidthsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +42,6 @@ export interface WidgetGraphComponentProps {
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||
enableDrillDown?: boolean;
|
||||
/** Hide list-panel pagination controls (e.g. public dashboards, where paging isn't supported). */
|
||||
hidePagination?: boolean;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -189,6 +189,8 @@ function Hosts(): JSX.Element {
|
||||
queryKeyPrefix="hosts"
|
||||
tabsConfig={{
|
||||
showEvents: false,
|
||||
showContainers: true,
|
||||
showProcesses: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { listHosts } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import K8sBaseDetails, {
|
||||
K8sDetailsFilters,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
|
||||
import StatusFilter from 'container/InfraMonitoringHostsV2/StatusFilter';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
getHostMetricsQueryPayload,
|
||||
hostDetailsMetadataConfig,
|
||||
hostGetEntityName,
|
||||
hostGetSelectedItemExpression,
|
||||
hostInitialEventsExpression,
|
||||
hostInitialLogTracesExpression,
|
||||
hostWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
getHostItemKey,
|
||||
getHostRowKey,
|
||||
hostColumnsConfig,
|
||||
} from './table.config';
|
||||
import { getHostsQuickFiltersConfig } from './utils';
|
||||
|
||||
import styles from './InfraMonitoringHosts.module.scss';
|
||||
import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const { redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
return;
|
||||
}
|
||||
isInitialized.current = true;
|
||||
|
||||
if (!compositeQuery) {
|
||||
const defaultQuery = initialQueriesMap[DataSource.METRICS];
|
||||
redirectWithQueryBuilderData({
|
||||
...defaultQuery,
|
||||
builder: {
|
||||
...defaultQuery.builder,
|
||||
queryData: defaultQuery.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
filter: { expression: '' },
|
||||
filters: { items: [], op: 'AND' as const },
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [compositeQuery, redirectWithQueryBuilderData]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
try {
|
||||
const response = await listHosts(
|
||||
{
|
||||
filter: {
|
||||
expression: filters.filter.expression,
|
||||
filterByStatus: filters.filter.filterByStatus
|
||||
? (filters.filter.filterByStatus as InframonitoringtypesHostStatusDTO)
|
||||
: undefined,
|
||||
},
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch hosts';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesHostRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: InframonitoringtypesHostRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listHosts(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch host';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getInitialLogTracesExpression = useCallback(
|
||||
(host: InframonitoringtypesHostRecordDTO) =>
|
||||
hostInitialLogTracesExpression(host, dotMetricsEnabled),
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
const controlListPrefix = !showFilters ? (
|
||||
<div className={styles.quickFiltersToggleContainer}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
<div className={styles.infraContentRow}>
|
||||
{showFilters && (
|
||||
<div className={styles.quickFiltersContainer}>
|
||||
<div className={styles.quickFiltersContainerHeader}>
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<ArrowUpToLine
|
||||
style={{ rotate: '270deg', cursor: 'pointer' }}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
size="md"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.listContainer}${
|
||||
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
|
||||
}`}
|
||||
>
|
||||
<K8sBaseList<InframonitoringtypesHostRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
leftFilters={<StatusFilter />}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
tableColumns={hostColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
getRowKey={getHostRowKey}
|
||||
getItemKey={getHostItemKey}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<K8sBaseDetails
|
||||
category={InfraMonitoringEntity.HOSTS}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
getSelectedItemExpression={hostGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={hostGetEntityName}
|
||||
getInitialLogTracesExpression={getInitialLogTracesExpression}
|
||||
getInitialEventsExpression={hostInitialEventsExpression}
|
||||
metadataConfig={hostDetailsMetadataConfig}
|
||||
entityWidgetInfo={hostWidgetInfo}
|
||||
getEntityQueryPayload={getHostMetricsQueryPayload}
|
||||
queryKeyPrefix="hosts"
|
||||
tabsConfig={{
|
||||
showEvents: false,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Hosts;
|
||||
@@ -1,161 +0,0 @@
|
||||
.infraMonitoringContainer {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.infraContentRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100vh - 45px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
:global(.periscope-btn) {
|
||||
&.ghost:not(:disabled) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
color: var(--bg-robin-500) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.quickFiltersContainer {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--l1-border);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
}
|
||||
|
||||
:global(.ant-collapse-header) {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
:global(.ant-collapse-content-box) {
|
||||
padding: 0 !important;
|
||||
padding-block: 0 !important;
|
||||
|
||||
:global(.quick-filters .checkbox-filter) {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.quick-filters) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quickFiltersContainerHeader {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 595px;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
max-width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.quickFiltersToggleContainer {
|
||||
grid-area: quickFilters;
|
||||
}
|
||||
|
||||
.infraMonitoringTags {
|
||||
width: fit-content;
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
border-radius: 50px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.tagsActive {
|
||||
color: var(--bg-forest-500, #25e192);
|
||||
border: 1px solid rgba(37, 225, 146, 0.2);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
}
|
||||
|
||||
.tagsInactive {
|
||||
color: var(--bg-slate-50, #62687c);
|
||||
border: 1px solid rgba(98, 104, 124, 0.2);
|
||||
background: rgba(98, 104, 124, 0.1);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
hostWidgetInfo,
|
||||
} from 'container/LogDetailedView/InfraMetrics/constants';
|
||||
|
||||
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
|
||||
|
||||
export function getProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export function getMemoryProgressColor(percent: number): string {
|
||||
if (percent >= 90) {
|
||||
return Color.BG_CHERRY_500;
|
||||
}
|
||||
if (percent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export type HostDetailMetadataConfigType =
|
||||
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
|
||||
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
{
|
||||
label: 'STATUS',
|
||||
getValue: (h): string =>
|
||||
h.status === InframonitoringtypesHostStatusDTO.active
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
render: (value, h): React.ReactNode => {
|
||||
const isActive = h.status === InframonitoringtypesHostStatusDTO.active;
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${infraHostsStyles.infraMonitoringTags} ${
|
||||
isActive ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'OPERATING SYSTEM',
|
||||
getValue: (h): string => h.meta?.['os.type'] || '-',
|
||||
render: (value): React.ReactNode =>
|
||||
value !== '-' ? (
|
||||
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
|
||||
{value}
|
||||
</Badge>
|
||||
) : (
|
||||
<Typography.Text>-</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'CPU USAGE',
|
||||
getValue: (h): number => h.cpu * 100,
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
strokeColor={getProgressColor(Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'MEMORY USAGE',
|
||||
getValue: (h): number => h.memory * 100,
|
||||
render: (value): React.ReactNode => (
|
||||
<Progress
|
||||
percent={Number(Number(value).toFixed(1))}
|
||||
strokeColor={getMemoryProgressColor(Number(value))}
|
||||
showInfo
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export function getHostMetricsQueryPayload(
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): ReturnType<typeof getHostQueryPayload> {
|
||||
return getHostQueryPayload(host.hostName, start, end, dotMetricsEnabled);
|
||||
}
|
||||
|
||||
export { hostWidgetInfo };
|
||||
|
||||
export const hostGetSelectedItemExpression = (hostName: string): string =>
|
||||
`host.name = ${formatValueForExpression(hostName)}`;
|
||||
|
||||
export function hostInitialLogTracesExpression(
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
dotMetricsEnabled: boolean,
|
||||
): string {
|
||||
const hostKey = dotMetricsEnabled
|
||||
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
|
||||
: 'host_name';
|
||||
const hostName = formatValueForExpression(host.hostName || '');
|
||||
return `${hostKey} = ${hostName}`;
|
||||
}
|
||||
|
||||
export function hostInitialEventsExpression(
|
||||
_host: InframonitoringtypesHostRecordDTO,
|
||||
): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
export const hostGetEntityName = (
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
): string => host.hostName;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import Hosts from './Hosts';
|
||||
|
||||
function InfraMonitoringHosts(): JSX.Element {
|
||||
const {
|
||||
updateAllQueriesOperators,
|
||||
handleSetConfig,
|
||||
setSupersetQuery,
|
||||
setLastUsedQuery,
|
||||
currentQuery,
|
||||
resetQuery,
|
||||
} = useQueryBuilder();
|
||||
|
||||
useEffect(() => {
|
||||
const newQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap.metrics,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.METRICS,
|
||||
);
|
||||
|
||||
setSupersetQuery(newQuery);
|
||||
setLastUsedQuery(0);
|
||||
handleSetConfig(PANEL_TYPES.TIME_SERIES, DataSource.METRICS);
|
||||
|
||||
return (): void => {
|
||||
setLastUsedQuery(0);
|
||||
};
|
||||
}, [
|
||||
updateAllQueriesOperators,
|
||||
setSupersetQuery,
|
||||
setLastUsedQuery,
|
||||
handleSetConfig,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const updatedCurrentQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
resetQuery(updatedCurrentQuery);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<Hosts />
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfraMonitoringHosts;
|
||||
@@ -1,297 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Container } from '@signozhq/icons';
|
||||
import {
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
ExpandButtonWrapper,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from 'container/InfraMonitoringK8sV2/components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { useInfraMonitoringGroupBy } from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import ColumnHeader from 'container/InfraMonitoringK8sV2/Base/ColumnHeader';
|
||||
import EntityGroupHeader from 'container/InfraMonitoringK8sV2/Base/EntityGroupHeader';
|
||||
|
||||
import { HostnameCell } from './utils';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
|
||||
const statusMap: Record<
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
{
|
||||
label: string;
|
||||
color: BadgeColor;
|
||||
}
|
||||
> = {
|
||||
[InframonitoringtypesHostStatusDTO.active]: {
|
||||
label: 'ACTIVE',
|
||||
color: 'forest',
|
||||
},
|
||||
[InframonitoringtypesHostStatusDTO.inactive]: {
|
||||
label: 'INACTIVE',
|
||||
color: 'amber',
|
||||
},
|
||||
['']: {
|
||||
label: 'UNKNOWN',
|
||||
color: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
function hostRowSource(host: InframonitoringtypesHostRecordDTO): {
|
||||
meta: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
meta: {
|
||||
...(host.meta ?? {}),
|
||||
[INFRA_MONITORING_ATTR_KEYS.HOST_NAME]: host.hostName ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getHostRowKey(host: InframonitoringtypesHostRecordDTO): string {
|
||||
return host.hostName || 'unknown';
|
||||
}
|
||||
|
||||
export function getHostItemKey(
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
): string {
|
||||
return host.hostName ?? '';
|
||||
}
|
||||
|
||||
function HostGroupCell({
|
||||
row,
|
||||
}: {
|
||||
row: InframonitoringtypesHostRecordDTO;
|
||||
}): JSX.Element {
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const synthetic = hostRowSource(row);
|
||||
return getGroupByEl(synthetic, groupBy) as JSX.Element;
|
||||
}
|
||||
|
||||
export type HostColumnConfigType =
|
||||
TableColumnDef<InframonitoringtypesHostRecordDTO>;
|
||||
export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
{
|
||||
id: 'hostGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Host Group" />,
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ row, isExpanded, toggleExpanded }): React.ReactNode => (
|
||||
<ExpandButtonWrapper isExpanded={isExpanded} toggleExpanded={toggleExpanded}>
|
||||
<HostGroupCell row={row} />
|
||||
</ExpandButtonWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'hostName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader
|
||||
title="Hostname"
|
||||
icon={<Container size={14} />}
|
||||
docPath="/infrastructure-monitoring/host-monitoring#hostname"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<HostnameCell hostName={value as string} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
tooltip="Sent system metrics in last 10 mins."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#status"
|
||||
>
|
||||
Status
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string => row.status,
|
||||
width: { min: 140 },
|
||||
enableSort: false,
|
||||
cell: ({ value, groupMeta, row }): React.ReactNode => {
|
||||
const status = value as InframonitoringtypesHostStatusDTO;
|
||||
|
||||
if (groupMeta) {
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.activeHostCount,
|
||||
label: 'Active',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.inactiveHostCount,
|
||||
label: 'Inactive',
|
||||
color: Color.BG_AMBER_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const statusDetails = statusMap[status] || statusMap[''];
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
color={statusDetails.color}
|
||||
className={`${styles.statusTag}`}
|
||||
>
|
||||
{statusDetails.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
|
||||
CPU Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.cpu,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<EntityProgressBar value={cpu} type="cpu" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader
|
||||
tooltip="Excluding cache memory."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
|
||||
className={styles.memoryUsageHeader}
|
||||
>
|
||||
Memory Usage (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.memory,
|
||||
width: { min: 240 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="memory metric"
|
||||
>
|
||||
<EntityProgressBar value={memory} type="memory" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'wait',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#iowait">
|
||||
IOWait
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.wait,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const wait = value as number;
|
||||
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={wait}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="IOWait metric"
|
||||
>
|
||||
<TanStackTable.Text>
|
||||
{`${Number((wait * 100).toFixed(1))}%`}
|
||||
</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'load15',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
|
||||
Load Avg (15min)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.load15,
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const load15 = Number(value);
|
||||
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={load15}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="load average metric"
|
||||
>
|
||||
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'diskUsage',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
|
||||
Disk Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.diskUsage,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const diskUsage = value as number;
|
||||
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={diskUsage}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="disk usage metric"
|
||||
>
|
||||
<EntityProgressBar value={diskUsage} type="disk" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,61 +0,0 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hostnameColumnHeader {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.columnHeaderRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.memoryUsageHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.statusTag {
|
||||
width: fit-content;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
border-radius: 50px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.statusTagActive {
|
||||
color: var(--Forest-500, #25e192);
|
||||
border: 1px solid rgba(37, 225, 146, 0.2);
|
||||
background: rgba(37, 225, 146, 0.1);
|
||||
}
|
||||
|
||||
.statusTagInactive {
|
||||
color: var(--Slate-50, #62687c);
|
||||
border: 1px solid rgba(98, 104, 124, 0.2);
|
||||
background: rgba(98, 104, 124, 0.1);
|
||||
}
|
||||
|
||||
.progressContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
|
||||
const HOSTNAME_DOCS_URL =
|
||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
|
||||
|
||||
export function HostnameCell({
|
||||
hostName,
|
||||
}: {
|
||||
hostName?: string | null;
|
||||
}): React.ReactElement {
|
||||
const isEmpty = !hostName || !hostName.trim();
|
||||
if (!isEmpty) {
|
||||
return (
|
||||
<CellValueTooltip value={hostName}>
|
||||
<TanStackTable.Text>{hostName}</TanStackTable.Text>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Typography.Text color="muted">-</Typography.Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
Missing host.name metadata.
|
||||
<br />
|
||||
<a
|
||||
href={HOSTNAME_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
Learn how to configure →
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
trigger={['hover', 'focus']}
|
||||
>
|
||||
<span
|
||||
className="hostname-cell-warning-icon"
|
||||
tabIndex={0}
|
||||
role="img"
|
||||
aria-label="Missing host.name metadata"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getHostsQuickFiltersConfig(
|
||||
dotMetricsEnabled: boolean,
|
||||
): IQuickFiltersConfig[] {
|
||||
const hostNameKey = dotMetricsEnabled
|
||||
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
|
||||
: 'host_name';
|
||||
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.15m'
|
||||
: 'system_cpu_load_average_15m';
|
||||
|
||||
const environmentKey = dotMetricsEnabled
|
||||
? 'deployment.environment'
|
||||
: 'deployment_environment';
|
||||
|
||||
return [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Host Name',
|
||||
attributeKey: {
|
||||
key: hostNameKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: metricName,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'OS Type',
|
||||
attributeKey: {
|
||||
key: osTypeKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: metricName,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: environmentKey,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
Package2,
|
||||
ScrollText,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
@@ -53,10 +54,12 @@ import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
|
||||
import EntityContainers from '../EntityDetailsUtils/EntityContainers';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityProcesses from '../EntityDetailsUtils/EntityProcesses';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
@@ -117,6 +120,8 @@ export interface K8sBaseDetailsProps<T> {
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
showContainers?: boolean;
|
||||
showProcesses?: boolean;
|
||||
};
|
||||
customTabs?: Array<{
|
||||
key: string;
|
||||
@@ -254,6 +259,8 @@ export default function K8sBaseDetails<T>({
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
showContainers: false,
|
||||
showProcesses: false,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
@@ -568,6 +575,32 @@ export default function K8sBaseDetails<T>({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showContainers
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.CONTAINERS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<Package2 size={14} />
|
||||
Containers
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showProcesses
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.PROCESSES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Processes
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
@@ -647,6 +680,11 @@ export default function K8sBaseDetails<T>({
|
||||
initialExpression={eventsInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.CONTAINERS &&
|
||||
tabVisibility.showContainers && <EntityContainers />}
|
||||
{effectiveView === VIEW_TYPES.PROCESSES && tabVisibility.showProcesses && (
|
||||
<EntityProcesses />
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<React.Fragment key={tab.key}>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
|
||||
|
||||
import broomUrl from '@/assets/Icons/broom.svg';
|
||||
import infraContainersUrl from '@/assets/Icons/infraContainers.svg';
|
||||
|
||||
import 'components/HostMetricsDetail/Containers/Containers.styles.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function EntityContainers(): JSX.Element {
|
||||
const { t } = useTranslation(['infraMonitoring']);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="host-containers" size={24}>
|
||||
<div className="infra-container-card-container">
|
||||
<div className="dev-status-container">
|
||||
<div className="infra-container-card">
|
||||
<img
|
||||
src={infraContainersUrl}
|
||||
alt="infra-container"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
|
||||
<Text className="infra-container-card-text">
|
||||
{t('containers_visualization_message')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="infra-container-working-msg">
|
||||
<Space>
|
||||
<img src={broomUrl} alt="broom" width={24} height={24} />
|
||||
<Text className="infra-container-card-text">{t('working_message')}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WaitlistFragment entityType="containers" />
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityContainers;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
|
||||
|
||||
import broomUrl from '@/assets/Icons/broom.svg';
|
||||
import infraContainersUrl from '@/assets/Icons/infraContainers.svg';
|
||||
|
||||
import 'components/HostMetricsDetail/Processes/Processes.styles.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function EntityProcesses(): JSX.Element {
|
||||
const { t } = useTranslation(['infraMonitoring']);
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="host-processes" size={24}>
|
||||
<div className="infra-container-card-container">
|
||||
<div className="dev-status-container">
|
||||
<div className="infra-container-card">
|
||||
<img
|
||||
src={infraContainersUrl}
|
||||
alt="infra-container"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<Text className="infra-container-card-text">
|
||||
{t('processes_visualization_message')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="infra-container-working-msg">
|
||||
<Space>
|
||||
<img src={broomUrl} alt="broom" width={24} height={24} />
|
||||
<Text className="infra-container-card-text">{t('working_message')}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WaitlistFragment entityType="processes" />
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityProcesses;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { act, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import { renderEntityTraces, verifyQueryPayload } from './testUtils';
|
||||
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
|
||||
|
||||
describe('EntityTraces - Default Behavior', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithTracesResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch traces using V5 API on initial render', async () => {
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
verifyQueryPayload({
|
||||
payload: capturedPayloads[0],
|
||||
expectedOffset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass time range to API (converted to milliseconds)', async () => {
|
||||
const timeRange = { startTime: 1000, endTime: 2000 };
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces({ timeRange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
verifyQueryPayload({
|
||||
payload: capturedPayloads[0],
|
||||
expectedTimeRange: {
|
||||
start: timeRange.startTime * 1000,
|
||||
end: timeRange.endTime * 1000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should order results by timestamp desc', async () => {
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
|
||||
order?: Array<{ key: { name: string }; direction: string }>;
|
||||
};
|
||||
const timestampOrder = spec.order?.find((o) => o.key.name === 'timestamp');
|
||||
|
||||
expect(timestampOrder?.direction).toBe('desc');
|
||||
});
|
||||
|
||||
it('should use TRACES signal type', async () => {
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
const query = capturedPayloads[0].compositeQuery.queries[0];
|
||||
const spec = query?.spec as { signal?: string };
|
||||
expect(spec?.signal).toBe('traces');
|
||||
});
|
||||
|
||||
it('should use raw request type for traces list view', async () => {
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].requestType).toBe('raw');
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { act, screen } from 'tests/test-utils';
|
||||
|
||||
import { renderEntityTraces } from './testUtils';
|
||||
import { mockQueryRangeV5WithEmptyTraces } from '__tests__/query_range_v5.util';
|
||||
|
||||
describe('EntityTraces - Empty State', () => {
|
||||
it('should show empty state when no traces returned', async () => {
|
||||
mockQueryRangeV5WithEmptyTraces();
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show table when traces are empty', async () => {
|
||||
mockQueryRangeV5WithEmptyTraces();
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
|
||||
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show pagination controls when traces are empty', async () => {
|
||||
mockQueryRangeV5WithEmptyTraces();
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
|
||||
expect(screen.queryByText(/1 - 10/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { act, screen } from 'tests/test-utils';
|
||||
|
||||
import { renderEntityTraces } from './testUtils';
|
||||
import {
|
||||
mockQueryRangeV5WithError,
|
||||
mockQueryRangeV5WithKeyNotFoundError,
|
||||
} from '__tests__/query_range_v5.util';
|
||||
|
||||
describe('EntityTraces - Error State', () => {
|
||||
it('should show error state on API error', async () => {
|
||||
mockQueryRangeV5WithError('Internal server error', 500);
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByText(/Something went wrong/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show empty state for key not found error', async () => {
|
||||
mockQueryRangeV5WithKeyNotFoundError();
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show table on API error', async () => {
|
||||
mockQueryRangeV5WithError('Internal server error', 500);
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByText(/Something went wrong/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import { act, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import { renderEntityTraces } from './testUtils';
|
||||
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
|
||||
|
||||
describe('EntityTraces - Expression', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithTracesResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should include expression in query filter', async () => {
|
||||
const expression = 'k8s.pod.name = "my-pod"';
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces({ expression });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
|
||||
filter?: { expression: string };
|
||||
};
|
||||
expect(spec.filter?.expression).toContain('k8s.pod.name');
|
||||
});
|
||||
|
||||
it('should include complex expression with multiple conditions', async () => {
|
||||
const expression = 'k8s.pod.name = "my-pod" AND service.name = "api"';
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces({ expression });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
|
||||
filter?: { expression: string };
|
||||
};
|
||||
expect(spec.filter?.expression).toContain('k8s.pod.name');
|
||||
expect(spec.filter?.expression).toContain('service.name');
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user