Compare commits

...

25 Commits

Author SHA1 Message Date
Aditya Singh
903912aa0f Feat/table enhancement (#10773)
* feat: add width for remaining space for col

* feat: style fixes

* feat: do not move state indicator in table

* feat: minor changes
2026-03-31 13:20:30 +05:30
Aditya Singh
32be778b03 Merge branch 'main' into SIG_3791 2026-03-31 13:15:04 +05:30
Amaresh S M
7b6f77bd52 fix(devenv): fix otel-collector startup failure (#10620)
Co-authored-by: Nageshbansal <76246968+Nageshbansal@users.noreply.github.com>
2026-03-31 07:08:41 +00:00
Pandey
e588c57e44 feat(audit): add noop auditor for community edition (#10769)
* feat(audit): add noop auditor and embed ServiceWithHealthy in Auditor

Embed factory.ServiceWithHealthy in the Auditor interface so all
providers (noop and future OTLP HTTP) share a uniform lifecycle
contract. Add pkg/auditor/noopauditor for the community edition
that silently discards all events with zero allocations.

* feat(audit): remove noopauditor test file
2026-03-31 06:16:20 +00:00
Ayush Shukla
98f53423dc docs: fix typo 'versinoing' -> 'versioning' in frontend README (#10765) 2026-03-31 05:05:23 +00:00
Pandey
e41d400aa0 feat(audit): add Auditor interface and rename auditortypes to audittypes (#10766)
Some checks failed
build-staging / prepare (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* feat(audit): add Auditor interface and rename auditortypes to audittypes

Add the Auditor interface in pkg/auditor/ as the contract between the
HTTP handler layer and audit implementations (noop for community, OTLP
for enterprise). Includes Config with buffer, batch, and flush settings
following the provider pattern.

Rename pkg/types/auditortypes/ to pkg/types/audittypes/ for consistency.

closes SigNoz/platform-pod#1930

* refactor(audit): move endpoint config to OTLPHTTPConfig provider struct

Move Endpoint out of top-level Config into a provider-specific
OTLPHTTPConfig struct with full OTLP HTTP options (url_path, insecure,
compression, timeout, headers, retry). Keep BufferSize, BatchSize,
FlushInterval at top level as common settings across providers.

closes SigNoz/platform-pod#1930

* feat(audit): add auditorbatcher for buffered event batching

Add pkg/auditor/auditorbatcher/ with channel-based batching for audit
events. Flushes when either batch size is reached or flush interval
elapses (whichever comes first). Events are dropped when the buffer is
full (fail-open). Follows the alertmanagerbatcher pattern.

* refactor(audit): replace public channel with Receive method on batcher

Make batchC private and expose Receive() <-chan []AuditEvent as the
read-side API. Clearer contract: Add() to write, Receive() to read.

* refactor(audit): rename batcher config fields to BufferSize and BatchSize

Capacity → BufferSize, Size → BatchSize for clarity.

* fix(audit): single-line slog call and fix log key to buffer_size

* feat(audit): add OTel metrics to auditorbatcher

Add telemetry via OTel MeterProvider with 4 instruments:
- signoz.audit.events.emitted (counter)
- signoz.audit.store.write_errors (counter, via RecordWriteError)
- signoz.audit.events.dropped (counter)
- signoz.audit.events.buffer_size (observable gauge)

Batcher.New() now accepts metric.Meter and returns error.

* refactor(audit): inject factory.ScopedProviderSettings into batcher

Replace separate logger and meter params with ScopedProviderSettings,
giving the batcher access to logger, meter, and tracer from one source.

* feat(audit): add OTel tracing to batcher Add path

Span auditorbatcher.Add with event_name attribute set at creation
and audit.dropped set dynamically on buffer-full drop.

* feat(audit): add export span to batcher Receive path

Introduce Batch struct carrying context, events, and a trace span.
Each flush starts an auditorbatcher.Export span with batch_size
attribute. The consumer ends the span after export completes.

* refactor(audit): replace Batch/Receive with ExportFunc callback

Batcher now takes an ExportFunc at construction and manages spans
internally. Removes Batch struct, Receive(), and RecordWriteError()
from the public API. Span.End() is always called via defer, write
errors and span status are recorded automatically on export failure.
Uses errors.Attr for error logging, prefixes log keys with audit.

* refactor(audit): rename auditorbatcher to auditorserver

Rename package, file (batcher.go → server.go), type (Batcher → Server),
and receiver (b → server) to reflect the full service role: buffering,
batching, metrics, tracing, and export lifecycle management.

* refactor(audit): rename telemetry to serverMetrics and document struct fields

Rename type telemetry → serverMetrics and constructor
newTelemetry → newServerMetrics. Add comments to all Server
struct fields.

* feat(audit): implement ServiceWithHealthy, fix race, add unit tests

- Implement factory.ServiceWithHealthy on Server via healthyC channel
- Fix data race: Start closes healthyC after goroutinesWg.Add(1),
  Stop waits on healthyC before closing stopC
- Add 8 unit tests covering construction, start/stop lifecycle,
  batch-size flush, interval flush, buffer-full drop, drain on stop,
  export failure handling, and concurrent safety

* fix(audit): fix lint issues in auditorserver and tests

Use snake_case for slog keys, errors.New instead of fmt.Errorf,
and check all Start/Stop return values in tests.

* fix(audit): address PR review comments

Use auditor:: prefix in validation error messages. Move fire-and-forget
comment to the Audit method, remove interface-level comment.
2026-03-30 20:46:38 +00:00
Vikrant Gupta
d19592ce7b chore(authz): bump up openfga version (#10767)
* chore(authz): bump up openfga version

* chore(authz): fix tests

* chore(authz): bump up openfga version

* chore(authz): remove ee references
2026-03-30 20:44:11 +00:00
Phuc
1f43feaf3c refactor(time-range): replace hardcoded 30m defaults with const (#10748)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* refactor: replace hardcoded 30m defaults with DEFAULT_TIME_RANGE

* refactor: use DEFAULT_TIME_RANGE in home.tsx

* revert: restore mock data before DEFAULT_TIME_RANGE changes
2026-03-30 15:49:01 +00:00
Pandey
37d202de92 feat(audit): add auditortypes package (#10761)
* feat(audit): add auditortypes package with event struct, store interface, and config

Foundational type package for the audit log system with zero dependencies
on other audit packages. Includes AuditEvent struct, Store interface (single
Emit method), Config with defaults, constants for action/outcome/principal/category,
and EventName derivation helper.

* fix(audit): address PR review feedback on auditortypes

- Remove non-CUD actions (login/logout/lock/unlock/revoke), keep only create/update/delete
- Replace pastTenseMap with pastTense field on Action struct
- Make EventName a typed struct with NewEventName() constructor
- Rename enum vars to ActionCategory* prefix (ActionCategoryAccessControl, etc.)
- Add IEC 62443 reference link to ActionCategory comment
- Add TODO on PrincipalType to use coretypes once available
- Rename types.go to event.go, merge Store interface into it
- Reorder AuditEvent fields to follow OTel LogRecord structure
- Delete config.go (belongs with auditor service, not types)
- Delete store.go (merged into event.go)

* fix(audit): remove redundant test files per review feedback
2026-03-30 14:46:50 +00:00
Vinicius Lourenço
87e5ef2f0a refactor(infrastructure-monitoring): use nuqs hooks (#10640)
* fix(infra-monitoring): use nuqs instead of searchParams to handle decoding issues

* fix(infra-monitoring): grouped row key trying to serialize circular reference

* fix(hooks): use push instead of replace to preserve back/forward history

* fix(infra-monitoring): preserve default order by from base query when value is null

* fix(pr-comments): tests and unused code

* fix(infra-monitoring): ensure all the pages uses/inherit base query order by

* fix(jsons): remove mistaken files added by accident

* fix(infra-monitoring): not resetting the filters after navigate

* fix(search-params): remove searchParams hook and prefer just location.search
2026-03-30 14:41:51 +00:00
Vikrant Gupta
b151bcd697 chore(authz): add error logger for batch check (#10756)
* chore(authz): add error logger for batch check

* chore(authz): add error logger for batch check
2026-03-30 10:50:34 +00:00
Srikanth Chekuri
bb4e7df68b chore: add rule state history module (#10488)
* chore: add rule state history module

* chore: run generate

* chore: generate

* Fix timeline default limit and escape exists key (#10490)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* chore: remove unused AddRuleStateHistory and add comments

* chore: regenerate

* chore: update names and move functions

* chore: remove return

* chore: update .github/CODEOWNERS for history

* chore: update condition builder

* chore: lint

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-03-30 10:38:52 +00:00
Yunus M
198b54252d chore: support cmd click on all clickable items (#10350)
* chore: support cmd click on all clickable items

* chore: update test cases

* feat: address review comments

* feat: enhance navigation tests for middle mouse button interactions

* refactor: update navigation handling to use safeNavigate with newTab option

* feat: support opening links in new tab with modifier key in K8sPodsList and SideNav

* chore: clean up environment variable usage in login utility and remove unused test file

* refactor: update import order and adjust navigation call in AlertNotFound tests

* chore: remove eslint disable

* chore: move isModiferKey to app util file

* feat: update buildAbsolutePath to handle empty relative path

* chore: fix import error
2026-03-30 08:43:41 +00:00
Ishan Uniyal
5845a4f624 feat: pr ready, styling folder and unwanted code removal 2026-03-26 09:44:54 +05:30
Ishan Uniyal
6e6e79104a feat: cols persistant issue 2026-03-25 08:00:10 +05:30
Ishan Uniyal
7378225f0e feat: css and cols persistant issue 2026-03-25 07:40:05 +05:30
Ishan Uniyal
29f6e99837 feat: scss prettier fix 2026-03-24 16:58:41 +05:30
Ishan Uniyal
4c6ca124ba feat: remove column and ux fixes 2026-03-24 16:46:28 +05:30
Ishan Uniyal
1e1bb043bb feat: row merging header fix 2026-03-24 16:46:28 +05:30
Ishan Uniyal
8ce4c016d8 feat: fixed scroll issue on drag, ux issue on cols and made scrollbar same 2026-03-24 16:46:28 +05:30
Ishan Uniyal
b9975ed128 feat: fixed settings 2026-03-24 16:46:28 +05:30
Ishan Uniyal
325cddfab8 feat: ux improve 2026-03-24 16:46:28 +05:30
Ishan Uniyal
f165913f21 feat: testcases and ux improve 2026-03-24 16:46:28 +05:30
Ishan Uniyal
1b310eb532 feat: ux and smoothness improved 2026-03-24 16:46:28 +05:30
Ishan Uniyal
9af2f3ef75 feat: table migration 2026-03-24 16:46:28 +05:30
197 changed files with 10055 additions and 2427 deletions

View File

@@ -27,8 +27,8 @@ services:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
- ${PWD}/../../../deploy/common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
ports:
- '127.0.0.1:8123:8123'
- '127.0.0.1:9000:9000'
- "127.0.0.1:8123:8123"
- "127.0.0.1:9000:9000"
tty: true
healthcheck:
test:
@@ -47,13 +47,16 @@ services:
condition: service_healthy
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
networks:
- default
- signoz-devenv
zookeeper:
image: signoz/zookeeper:3.7.1
container_name: zookeeper
volumes:
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
ports:
- '127.0.0.1:2181:2181'
- "127.0.0.1:2181:2181"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
healthcheck:
@@ -74,12 +77,19 @@ services:
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
depends_on:
clickhouse:
condition: service_healthy
restart: on-failure
networks:
- default
- signoz-devenv
networks:
signoz-devenv:
name: signoz-devenv

View File

@@ -3,7 +3,7 @@ services:
image: signoz/signoz-otel-collector:v0.142.0
container_name: signoz-otel-collector-dev
entrypoint:
- /bin/sh
- /bin/sh
command:
- -c
- |
@@ -34,4 +34,11 @@ services:
retries: 3
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
- "host.docker.internal:host-gateway"
networks:
- default
- signoz-devenv
networks:
signoz-devenv:
name: signoz-devenv

View File

@@ -12,10 +12,10 @@ receivers:
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
@@ -29,7 +29,26 @@ processors:
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
latency_histogram_buckets:
[
100us,
1ms,
2ms,
6ms,
10ms,
50ms,
100ms,
250ms,
500ms,
1000ms,
1400ms,
2000ms,
5s,
10s,
20s,
40s,
60s,
]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
@@ -60,13 +79,13 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://host.docker.internal:9000/signoz_traces
datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://host.docker.internal:9000/signoz_metrics
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://host.docker.internal:9000/signoz_logs
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
use_new_schema: true
@@ -93,4 +112,4 @@ service:
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter]
exporters: [clickhouselogsexporter]

4
.github/CODEOWNERS vendored
View File

@@ -86,6 +86,8 @@ go.mod @therealpandey
/pkg/types/alertmanagertypes @srikanthccv
/pkg/alertmanager/ @srikanthccv
/pkg/ruler/ @srikanthccv
/pkg/modules/rulestatehistory/ @srikanthccv
/pkg/types/rulestatehistorytypes/ @srikanthccv
# Correlation-adjacent
@@ -105,7 +107,7 @@ go.mod @therealpandey
/pkg/modules/authdomain/ @vikrantgupta25
/pkg/modules/role/ @vikrantgupta25
# IdentN Owners
# IdentN Owners
/pkg/identn/ @vikrantgupta25
/pkg/http/middleware/identn.go @vikrantgupta25

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
@@ -78,8 +79,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
if err != nil {
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
"github.com/SigNoz/signoz/ee/authz/openfgaserver"
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
@@ -118,8 +119,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return authNs, nil
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), licensing, dashboardModule)
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
if err != nil {
return nil, err
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, dashboardModule), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)

View File

@@ -2279,6 +2279,140 @@ components:
- status
- error
type: object
RulestatehistorytypesAlertState:
enum:
- inactive
- pending
- recovering
- firing
- nodata
- disabled
type: string
RulestatehistorytypesGettableRuleStateHistory:
properties:
fingerprint:
minimum: 0
type: integer
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
nullable: true
type: array
overallState:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
overallStateChanged:
type: boolean
ruleID:
type: string
ruleName:
type: string
state:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
stateChanged:
type: boolean
unixMilli:
format: int64
type: integer
value:
format: double
type: number
required:
- ruleID
- ruleName
- overallState
- overallStateChanged
- state
- stateChanged
- unixMilli
- labels
- fingerprint
- value
type: object
RulestatehistorytypesGettableRuleStateHistoryContributor:
properties:
count:
minimum: 0
type: integer
fingerprint:
minimum: 0
type: integer
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
nullable: true
type: array
relatedLogsLink:
type: string
relatedTracesLink:
type: string
required:
- fingerprint
- labels
- count
type: object
RulestatehistorytypesGettableRuleStateHistoryStats:
properties:
currentAvgResolutionTime:
format: double
type: number
currentAvgResolutionTimeSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
currentTriggersSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
pastAvgResolutionTime:
format: double
type: number
pastAvgResolutionTimeSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
pastTriggersSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
totalCurrentTriggers:
minimum: 0
type: integer
totalPastTriggers:
minimum: 0
type: integer
required:
- totalCurrentTriggers
- totalPastTriggers
- currentTriggersSeries
- pastTriggersSeries
- currentAvgResolutionTime
- pastAvgResolutionTime
- currentAvgResolutionTimeSeries
- pastAvgResolutionTimeSeries
type: object
RulestatehistorytypesGettableRuleStateTimeline:
properties:
items:
items:
$ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateHistory'
nullable: true
type: array
nextCursor:
type: string
total:
minimum: 0
type: integer
required:
- items
- total
type: object
RulestatehistorytypesGettableRuleStateWindow:
properties:
end:
format: int64
type: integer
start:
format: int64
type: integer
state:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
required:
- state
- start
- end
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
@@ -7923,6 +8057,518 @@ paths:
summary: Get users by role id
tags:
- users
/api/v2/rules/{id}/history/filter_keys:
get:
deprecated: false
description: Returns distinct label keys from rule history entries for the selected
range.
operationId: GetRuleHistoryFilterKeys
parameters:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
- in: query
name: limit
schema:
type: integer
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
- in: query
name: metricName
schema:
type: string
- in: query
name: searchText
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableFieldKeys'
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: Get rule history filter keys
tags:
- rules
/api/v2/rules/{id}/history/filter_values:
get:
deprecated: false
description: Returns distinct label values for a given key from rule history
entries.
operationId: GetRuleHistoryFilterValues
parameters:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
- in: query
name: limit
schema:
type: integer
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
- in: query
name: metricName
schema:
type: string
- in: query
name: searchText
schema:
type: string
- in: query
name: name
schema:
type: string
- in: query
name: existingQuery
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableFieldValues'
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: Get rule history filter values
tags:
- rules
/api/v2/rules/{id}/history/overall_status:
get:
deprecated: false
description: Returns overall firing/inactive intervals for a rule in the selected
time range.
operationId: GetRuleHistoryOverallStatus
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateWindow'
nullable: true
type: array
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: Get rule overall status timeline
tags:
- rules
/api/v2/rules/{id}/history/stats:
get:
deprecated: false
description: Returns trigger and resolution statistics for a rule in the selected
time range.
operationId: GetRuleHistoryStats
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateHistoryStats'
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: Get rule history stats
tags:
- rules
/api/v2/rules/{id}/history/timeline:
get:
deprecated: false
description: Returns paginated timeline entries for rule state transitions.
operationId: GetRuleHistoryTimeline
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: query
name: state
schema:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
- in: query
name: filterExpression
schema:
type: string
- in: query
name: limit
schema:
format: int64
type: integer
- in: query
name: order
schema:
$ref: '#/components/schemas/Querybuildertypesv5OrderDirection'
- in: query
name: cursor
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateTimeline'
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: Get rule history timeline
tags:
- rules
/api/v2/rules/{id}/history/top_contributors:
get:
deprecated: false
description: Returns top label combinations contributing to rule firing in the
selected time range.
operationId: GetRuleHistoryTopContributors
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateHistoryContributor'
nullable: true
type: array
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: Get top contributors to rule firing
tags:
- rules
/api/v2/sessions:
delete:
deprecated: false

View File

@@ -16,6 +16,7 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
"github.com/openfga/openfga/pkg/storage"
)
type provider struct {
@@ -26,14 +27,14 @@ type provider struct {
registry []authz.RegisterTypeable
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, licensing licensing.Licensing, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, licensing, registry)
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, registry)
})
}
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, licensing licensing.Licensing, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema)
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore)
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
if err != nil {
return nil, err

View File

@@ -0,0 +1,32 @@
package openfgaserver
import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/openfga/openfga/pkg/storage"
"github.com/openfga/openfga/pkg/storage/postgres"
"github.com/openfga/openfga/pkg/storage/sqlcommon"
"github.com/openfga/openfga/pkg/storage/sqlite"
)
func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) {
switch store.BunDB().Dialect().Name().String() {
case "sqlite":
return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{
MaxTuplesPerWriteField: 100,
MaxTypesPerModelField: 100,
})
case "pg":
pgStore, ok := store.(postgressqlstore.Pooler)
if !ok {
panic(errors.New(errors.TypeInternal, errors.CodeInternal, "postgressqlstore should implement Pooler"))
}
return postgres.NewWithDB(pgStore.Pool(), nil, &sqlcommon.Config{
MaxTuplesPerWriteField: 100,
MaxTypesPerModelField: 100,
})
}
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid store type: %s", store.BunDB().Dialect().Name().String())
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -106,6 +107,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
signoz.TelemetryMetadataStore,
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Modules.RuleStateHistory,
signoz.Querier,
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
@@ -344,28 +346,29 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &baserules.ManagerOptions{
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Reader: ch,
Querier: querier,
Logger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Reader: ch,
Querier: querier,
Logger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
RuleStateHistoryModule: ruleStateHistoryModule,
}
// create Manager

View File

@@ -28,6 +28,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@@ -41,6 +42,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -65,6 +67,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -90,6 +93,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err

View File

@@ -14,14 +14,21 @@ import (
"github.com/uptrace/bun/dialect/pgdialect"
)
var _ Pooler = new(provider)
type provider struct {
settings factory.ScopedProviderSettings
sqldb *sql.DB
bundb *sqlstore.BunDB
pgxPool *pgxpool.Pool
dialect *dialect
formatter sqlstore.SQLFormatter
}
type Pooler interface {
Pool() *pgxpool.Pool
}
func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] {
return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlstore.Config) (sqlstore.SQLStore, error) {
hooks := make([]sqlstore.SQLStoreHook, len(hookFactories))
@@ -62,6 +69,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
settings: settings,
sqldb: sqldb,
bundb: bunDB,
pgxPool: pool,
dialect: new(dialect),
formatter: newFormatter(bunDB.Dialect()),
}, nil
@@ -75,6 +83,10 @@ func (provider *provider) SQLDB() *sql.DB {
return provider.sqldb
}
func (provider *provider) Pool() *pgxpool.Pool {
return provider.pgxPool
}
func (provider *provider) Dialect() sqlstore.SQLDialect {
return provider.dialect
}

View File

@@ -12,7 +12,7 @@
or
`docker build . -t tagname`
**Tag to remote url- Introduce versinoing later on**
**Tag to remote url- Introduce versioning later on**
```
docker tag signoz/frontend:latest 7296823551/signoz:latest

View File

@@ -2,6 +2,7 @@
interface SafeNavigateOptions {
replace?: boolean;
state?: unknown;
newTab?: boolean;
}
interface SafeNavigateTo {
@@ -20,9 +21,7 @@ interface UseSafeNavigateReturn {
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
safeNavigate: jest.fn(
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
console.log(`Mock safeNavigate called with:`, to, options);
},
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
) as jest.MockedFunction<
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
>,

View File

@@ -68,8 +68,8 @@
"@signozhq/toggle-group": "0.0.1",
"@signozhq/tooltip": "0.0.2",
"@signozhq/ui": "0.0.5",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
@@ -164,6 +164,7 @@
"vite-plugin-html": "3.2.2",
"web-vitals": "^0.2.4",
"xstate": "^4.31.0",
"zod": "4.3.6",
"zustand": "5.0.11"
},
"browserslist": {
@@ -286,4 +287,4 @@
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

View File

@@ -1,6 +1,7 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
@@ -8,7 +9,7 @@ const createPublicDashboard = async (
props: CreatePublicDashboardProps,
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
try {
const response = await axios.post(

View File

@@ -1,6 +1,7 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
@@ -8,7 +9,7 @@ const updatePublicDashboard = async (
props: UpdatePublicDashboardProps,
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
try {
const response = await axios.put(

View File

@@ -0,0 +1,744 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
GetRuleHistoryFilterKeys200,
GetRuleHistoryFilterKeysParams,
GetRuleHistoryFilterKeysPathParameters,
GetRuleHistoryFilterValues200,
GetRuleHistoryFilterValuesParams,
GetRuleHistoryFilterValuesPathParameters,
GetRuleHistoryOverallStatus200,
GetRuleHistoryOverallStatusParams,
GetRuleHistoryOverallStatusPathParameters,
GetRuleHistoryStats200,
GetRuleHistoryStatsParams,
GetRuleHistoryStatsPathParameters,
GetRuleHistoryTimeline200,
GetRuleHistoryTimelineParams,
GetRuleHistoryTimelinePathParameters,
GetRuleHistoryTopContributors200,
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
/**
* Returns distinct label keys from rule history entries for the selected range.
* @summary Get rule history filter keys
*/
export const getRuleHistoryFilterKeys = (
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryFilterKeys200>({
url: `/api/v2/rules/${id}/history/filter_keys`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryFilterKeysQueryKey = (
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
) => {
return [
`/api/v2/rules/${id}/history/filter_keys`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryFilterKeysQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryFilterKeysQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
> = ({ signal }) => getRuleHistoryFilterKeys({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryFilterKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
>;
export type GetRuleHistoryFilterKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history filter keys
*/
export function useGetRuleHistoryFilterKeys<
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryFilterKeysQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history filter keys
*/
export const invalidateGetRuleHistoryFilterKeys = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryFilterKeysQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns distinct label values for a given key from rule history entries.
* @summary Get rule history filter values
*/
export const getRuleHistoryFilterValues = (
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryFilterValues200>({
url: `/api/v2/rules/${id}/history/filter_values`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryFilterValuesQueryKey = (
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
) => {
return [
`/api/v2/rules/${id}/history/filter_values`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryFilterValuesQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryFilterValuesQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
> = ({ signal }) => getRuleHistoryFilterValues({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryFilterValuesQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
>;
export type GetRuleHistoryFilterValuesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history filter values
*/
export function useGetRuleHistoryFilterValues<
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryFilterValuesQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history filter values
*/
export const invalidateGetRuleHistoryFilterValues = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryFilterValuesQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns overall firing/inactive intervals for a rule in the selected time range.
* @summary Get rule overall status timeline
*/
export const getRuleHistoryOverallStatus = (
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryOverallStatus200>({
url: `/api/v2/rules/${id}/history/overall_status`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryOverallStatusQueryKey = (
{ id }: GetRuleHistoryOverallStatusPathParameters,
params?: GetRuleHistoryOverallStatusParams,
) => {
return [
`/api/v2/rules/${id}/history/overall_status`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryOverallStatusQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryOverallStatusQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
> = ({ signal }) => getRuleHistoryOverallStatus({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryOverallStatusQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
>;
export type GetRuleHistoryOverallStatusQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule overall status timeline
*/
export function useGetRuleHistoryOverallStatus<
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryOverallStatusQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule overall status timeline
*/
export const invalidateGetRuleHistoryOverallStatus = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryOverallStatusQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns trigger and resolution statistics for a rule in the selected time range.
* @summary Get rule history stats
*/
export const getRuleHistoryStats = (
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryStats200>({
url: `/api/v2/rules/${id}/history/stats`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryStatsQueryKey = (
{ id }: GetRuleHistoryStatsPathParameters,
params?: GetRuleHistoryStatsParams,
) => {
return [
`/api/v2/rules/${id}/history/stats`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryStatsQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryStats>>
> = ({ signal }) => getRuleHistoryStats({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryStats>>
>;
export type GetRuleHistoryStatsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history stats
*/
export function useGetRuleHistoryStats<
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryStatsQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history stats
*/
export const invalidateGetRuleHistoryStats = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryStatsQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns paginated timeline entries for rule state transitions.
* @summary Get rule history timeline
*/
export const getRuleHistoryTimeline = (
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryTimeline200>({
url: `/api/v2/rules/${id}/history/timeline`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryTimelineQueryKey = (
{ id }: GetRuleHistoryTimelinePathParameters,
params?: GetRuleHistoryTimelineParams,
) => {
return [
`/api/v2/rules/${id}/history/timeline`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryTimelineQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryTimelineQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
> = ({ signal }) => getRuleHistoryTimeline({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryTimelineQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
>;
export type GetRuleHistoryTimelineQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history timeline
*/
export function useGetRuleHistoryTimeline<
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryTimelineQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history timeline
*/
export const invalidateGetRuleHistoryTimeline = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryTimelineQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns top label combinations contributing to rule firing in the selected time range.
* @summary Get top contributors to rule firing
*/
export const getRuleHistoryTopContributors = (
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryTopContributors200>({
url: `/api/v2/rules/${id}/history/top_contributors`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryTopContributorsQueryKey = (
{ id }: GetRuleHistoryTopContributorsPathParameters,
params?: GetRuleHistoryTopContributorsParams,
) => {
return [
`/api/v2/rules/${id}/history/top_contributors`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryTopContributorsQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryTopContributorsQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
> = ({ signal }) => getRuleHistoryTopContributors({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryTopContributorsQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
>;
export type GetRuleHistoryTopContributorsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get top contributors to rule firing
*/
export function useGetRuleHistoryTopContributors<
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryTopContributorsQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get top contributors to rule firing
*/
export const invalidateGetRuleHistoryTopContributors = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryTopContributorsQueryKey({ id }, params) },
options,
);
return queryClient;
};

View File

@@ -2677,6 +2677,139 @@ export interface RenderErrorResponseDTO {
status: string;
}
export enum RulestatehistorytypesAlertStateDTO {
inactive = 'inactive',
pending = 'pending',
recovering = 'recovering',
firing = 'firing',
nodata = 'nodata',
disabled = 'disabled',
}
export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
overallState: RulestatehistorytypesAlertStateDTO;
/**
* @type boolean
*/
overallStateChanged: boolean;
/**
* @type string
*/
ruleID: string;
/**
* @type string
*/
ruleName: string;
state: RulestatehistorytypesAlertStateDTO;
/**
* @type boolean
*/
stateChanged: boolean;
/**
* @type integer
* @format int64
*/
unixMilli: number;
/**
* @type number
* @format double
*/
value: number;
}
export interface RulestatehistorytypesGettableRuleStateHistoryContributorDTO {
/**
* @type integer
* @minimum 0
*/
count: number;
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
/**
* @type string
*/
relatedLogsLink?: string;
/**
* @type string
*/
relatedTracesLink?: string;
}
export interface RulestatehistorytypesGettableRuleStateHistoryStatsDTO {
/**
* @type number
* @format double
*/
currentAvgResolutionTime: number;
currentAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO;
currentTriggersSeries: Querybuildertypesv5TimeSeriesDTO;
/**
* @type number
* @format double
*/
pastAvgResolutionTime: number;
pastAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO;
pastTriggersSeries: Querybuildertypesv5TimeSeriesDTO;
/**
* @type integer
* @minimum 0
*/
totalCurrentTriggers: number;
/**
* @type integer
* @minimum 0
*/
totalPastTriggers: number;
}
export interface RulestatehistorytypesGettableRuleStateTimelineDTO {
/**
* @type array
* @nullable true
*/
items: RulestatehistorytypesGettableRuleStateHistoryDTO[] | null;
/**
* @type string
*/
nextCursor?: string;
/**
* @type integer
* @minimum 0
*/
total: number;
}
export interface RulestatehistorytypesGettableRuleStateWindowDTO {
/**
* @type integer
* @format int64
*/
end: number;
/**
* @type integer
* @format int64
*/
start: number;
state: RulestatehistorytypesAlertStateDTO;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
@@ -4312,6 +4445,266 @@ export type GetUsersByRoleID200 = {
status: string;
};
export type GetRuleHistoryFilterKeysPathParameters = {
id: string;
};
export type GetRuleHistoryFilterKeysParams = {
/**
* @description undefined
*/
signal?: TelemetrytypesSignalDTO;
/**
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
/**
* @description undefined
*/
fieldContext?: TelemetrytypesFieldContextDTO;
/**
* @description undefined
*/
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
searchText?: string;
};
export type GetRuleHistoryFilterKeys200 = {
data: TelemetrytypesGettableFieldKeysDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryFilterValuesPathParameters = {
id: string;
};
export type GetRuleHistoryFilterValuesParams = {
/**
* @description undefined
*/
signal?: TelemetrytypesSignalDTO;
/**
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
/**
* @description undefined
*/
fieldContext?: TelemetrytypesFieldContextDTO;
/**
* @description undefined
*/
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
searchText?: string;
/**
* @type string
* @description undefined
*/
name?: string;
/**
* @type string
* @description undefined
*/
existingQuery?: string;
};
export type GetRuleHistoryFilterValues200 = {
data: TelemetrytypesGettableFieldValuesDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryOverallStatusPathParameters = {
id: string;
};
export type GetRuleHistoryOverallStatusParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryOverallStatus200 = {
/**
* @type array
* @nullable true
*/
data: RulestatehistorytypesGettableRuleStateWindowDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryStatsPathParameters = {
id: string;
};
export type GetRuleHistoryStatsParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryStats200 = {
data: RulestatehistorytypesGettableRuleStateHistoryStatsDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryTimelinePathParameters = {
id: string;
};
export type GetRuleHistoryTimelineParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
/**
* @description undefined
*/
state?: RulestatehistorytypesAlertStateDTO;
/**
* @type string
* @description undefined
*/
filterExpression?: string;
/**
* @type integer
* @format int64
* @description undefined
*/
limit?: number;
/**
* @description undefined
*/
order?: Querybuildertypesv5OrderDirectionDTO;
/**
* @type string
* @description undefined
*/
cursor?: string;
};
export type GetRuleHistoryTimeline200 = {
data: RulestatehistorytypesGettableRuleStateTimelineDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryTopContributorsPathParameters = {
id: string;
};
export type GetRuleHistoryTopContributorsParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryTopContributors200 = {
/**
* @type array
* @nullable true
*/
data: RulestatehistorytypesGettableRuleStateHistoryContributorDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
@@ -49,6 +49,7 @@ import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
@@ -221,7 +222,7 @@ function LogDetailInner({
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (): void => {
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -234,7 +235,9 @@ function LogDetailInner({
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
newTab: !!e && isModifierKeyPressed(e),
});
};
const handleQueryExpressionChange = useCallback(

View File

@@ -28,14 +28,22 @@
}
}
// In table/column view, keep action buttons visible at the viewport's right edge
.log-line-action-buttons.table-view-log-actions {
position: absolute;
top: 50%;
right: 8px;
left: auto;
transform: translateY(-50%);
margin: 0;
z-index: 5;
}
.lightMode {
.log-line-action-buttons {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-400);
.ant-btn-default {
}
.copy-log-btn {
border-left: 1px solid var(--bg-vanilla-400);
border-color: var(--bg-vanilla-400) !important;

View File

@@ -15,13 +15,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
letterSpacing: '-0.07px',
marginBottom: '0px',
minWidth: '10rem',
width: '10rem',
width: 'auto',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '90rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -43,7 +43,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const bodyColumnStyle = useMemo(
() => ({
...defaultTableStyle,
...(fields.length > 2 ? { width: '50rem' } : {}),
...(fields.length > 2 ? { width: 'auto' } : {}),
}),
[fields.length],
);

View File

@@ -1,21 +1,35 @@
.quick-filters-container {
display: flex;
flex-direction: row;
height: 100%;
min-height: 0;
position: relative;
.quick-filters-settings-container {
flex: 0 0 0;
width: 0;
min-width: 0;
overflow: visible;
position: relative;
align-self: stretch;
}
}
.quick-filters {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
height: 100%;
width: 100%;
min-height: 0;
border-right: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-100);
.overlay-scrollbar {
flex: 1;
min-height: 0;
}
.header {
display: flex;
align-items: center;

View File

@@ -2,6 +2,8 @@
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 0;
z-index: 999;
width: 342px;
background: var(--bg-slate-500);

View File

@@ -12,6 +12,7 @@ export enum LOCALSTORAGE {
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
CHAT_SUPPORT = 'CHAT_SUPPORT',

View File

@@ -6,6 +6,7 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { isModifierKeyPressed } from 'utils/app';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
@@ -70,8 +71,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
</Tag>
) : undefined
}
onClick={(): void => {
onSelect(option.selection);
onClick={(e): void => {
onSelect(option.selection, isModifierKeyPressed(e));
}}
data-testid={`alert-type-card-${option.selection}`}
>
@@ -108,7 +109,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
}
interface SelectAlertTypeProps {
onSelect: (typ: AlertTypes) => void;
onSelect: (type: AlertTypes, newTab?: boolean) => void;
}
export default SelectAlertType;

View File

@@ -4,6 +4,7 @@ import { Button, Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Check, Loader, Send, X } from 'lucide-react';
import { isModifierKeyPressed } from 'utils/app';
import { useCreateAlertState } from '../context';
import {
@@ -33,9 +34,9 @@ function Footer(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const handleDiscard = (): void => {
const handleDiscard = (e: React.MouseEvent): void => {
discardAlertRule();
safeNavigate('/alerts');
safeNavigate('/alerts', { newTab: isModifierKeyPressed(e) });
};
const alertValidationMessage = useMemo(

View File

@@ -1,6 +1,7 @@
import { useCopyToClipboard } from 'react-use';
import { toast } from '@signozhq/sonner';
import { fireEvent, within } from '@testing-library/react';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { StatusCodes } from 'http-status-codes';
import {
publishedPublicDashboardMeta,
@@ -33,7 +34,6 @@ const mockToast = jest.mocked(toast);
// Test constants
const MOCK_DASHBOARD_ID = 'test-dashboard-id';
const MOCK_PUBLIC_PATH = '/public/dashboard/test-dashboard-id';
const DEFAULT_TIME_RANGE = '30m';
const DASHBOARD_VARIABLES_WARNING =
"Dashboard variables won't work in public dashboards";

View File

@@ -7,6 +7,7 @@ import { Button, Select, Typography } from 'antd';
import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard';
import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess';
import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react';
@@ -56,7 +57,7 @@ function PublicDashboardSetting(): JSX.Element {
PublicDashboardMetaProps | undefined
>(undefined);
const [timeRangeEnabled, setTimeRangeEnabled] = useState(true);
const [defaultTimeRange, setDefaultTimeRange] = useState('30m');
const [defaultTimeRange, setDefaultTimeRange] = useState(DEFAULT_TIME_RANGE);
const [, setCopyPublicDashboardURL] = useCopyToClipboard();
const { selectedDashboard } = useDashboardStore();
@@ -99,7 +100,7 @@ function PublicDashboardSetting(): JSX.Element {
console.error('Error getting public dashboard', errorPublicDashboard);
setPublicDashboardData(undefined);
setTimeRangeEnabled(true);
setDefaultTimeRange('30m');
setDefaultTimeRange(DEFAULT_TIME_RANGE);
}
}, [publicDashboardResponse, errorPublicDashboard]);
@@ -109,7 +110,7 @@ function PublicDashboardSetting(): JSX.Element {
publicDashboardResponse?.data?.timeRangeEnabled || false,
);
setDefaultTimeRange(
publicDashboardResponse?.data?.defaultTimeRange || '30m',
publicDashboardResponse?.data?.defaultTimeRange || DEFAULT_TIME_RANGE,
);
}
}, [publicDashboardResponse]);

View File

@@ -16,6 +16,8 @@ import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils';
import { useTimezone } from 'providers/Timezone';
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { keyToExclude } from './config';
import { DashedContainer, EditorContainer, EventContainer } from './styles';
@@ -111,14 +113,19 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
}));
const onClickTraceHandler = (): void => {
const onClickTraceHandler = (event: React.MouseEvent): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
const path = `/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`;
if (isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
history.push(path);
}
};
const logEventCalledRef = useRef(false);

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
@@ -44,6 +44,7 @@ import { QueryFunction } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
import BasicInfo from './BasicInfo';
@@ -330,13 +331,18 @@ function FormAlertRules({
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
const onCancelHandler = useCallback(
(e?: React.MouseEvent) => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
newTab: !!e && isModifierKeyPressed(e),
});
},
[safeNavigate, urlQuery],
);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults

View File

@@ -1,7 +1,13 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import {
LoadingOutlined,
SearchOutlined,
@@ -46,6 +52,7 @@ import {
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
@@ -290,9 +297,11 @@ function FullView({
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
if (dashboardEditView) {
safeNavigate(dashboardEditView);
safeNavigate(dashboardEditView, {
newTab: isModifierKeyPressed(e),
});
}
}}
>

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable sonarjs/no-duplicate-string */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
@@ -16,9 +17,11 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence } from 'motion/react';
@@ -29,6 +32,7 @@ import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { isIngestionActive } from 'utils/app';
import { isModifierKeyPressed } from 'utils/app';
import { popupContainer } from 'utils/selectPopupContainer';
import AlertRules from './AlertRules/AlertRules';
@@ -47,6 +51,7 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const isDarkMode = useIsDarkMode();
const [startTime, setStartTime] = useState<number | null>(null);
@@ -77,7 +82,7 @@ export default function Home(): JSX.Element {
query: initialQueriesMap[DataSource.LOGS],
graphType: PANEL_TYPES.VALUE,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: '30m',
globalSelectedInterval: DEFAULT_TIME_RANGE,
params: {
dataSource: DataSource.LOGS,
},
@@ -87,7 +92,7 @@ export default function Home(): JSX.Element {
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
'30m',
DEFAULT_TIME_RANGE,
endTime || Date.now(),
startTime || Date.now(),
initialQueriesMap[DataSource.LOGS],
@@ -102,7 +107,7 @@ export default function Home(): JSX.Element {
query: initialQueriesMap[DataSource.TRACES],
graphType: PANEL_TYPES.VALUE,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: '30m',
globalSelectedInterval: DEFAULT_TIME_RANGE,
params: {
dataSource: DataSource.TRACES,
},
@@ -112,7 +117,7 @@ export default function Home(): JSX.Element {
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
'30m',
DEFAULT_TIME_RANGE,
endTime || Date.now(),
startTime || Date.now(),
initialQueriesMap[DataSource.TRACES],
@@ -393,11 +398,14 @@ export default function Home(): JSX.Element {
role="button"
tabIndex={0}
className="active-ingestion-card-actions"
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
// eslint-disable-next-line sonarjs/no-duplicate-string
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
safeNavigate(ROUTES.LOGS_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -434,11 +442,13 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
safeNavigate(ROUTES.TRACES_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -475,11 +485,13 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER);
safeNavigate(ROUTES.METRICS_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -529,11 +541,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
safeNavigate(ROUTES.LOGS_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
>
Open Logs Explorer
@@ -543,11 +557,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
safeNavigate(ROUTES.TRACES_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
>
Open Traces Explorer
@@ -557,11 +573,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
safeNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, {
newTab: isModifierKeyPressed(e),
});
}}
>
Open Metrics Explorer
@@ -598,11 +616,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
history.push(ROUTES.ALL_DASHBOARD);
safeNavigate(ROUTES.ALL_DASHBOARD, {
newTab: isModifierKeyPressed(e),
});
}}
>
Create dashboard
@@ -640,11 +660,13 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
history.push(ROUTES.ALERTS_NEW);
safeNavigate(ROUTES.ALERTS_NEW, {
newTab: isModifierKeyPressed(e),
});
}}
>
Create an alert

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -30,6 +30,7 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
@@ -117,7 +118,7 @@ const ServicesListTable = memo(
onRowClick,
}: {
services: ServicesList[];
onRowClick: (record: ServicesList) => void;
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
}): JSX.Element => (
<div className="services-list-container home-data-item-container metrics-services-list">
<div className="services-list">
@@ -126,8 +127,8 @@ const ServicesListTable = memo(
dataSource={services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
})}
/>
</div>
@@ -285,11 +286,13 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList) => {
(record: ServicesList, event: React.MouseEvent) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, {
newTab: isModifierKeyPressed(event),
});
},
[safeNavigate],
);

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { Link } from 'react-router-dom';
import { Button, Select, Skeleton, Table } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -16,6 +16,7 @@ import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -173,13 +174,15 @@ export default function ServiceTraces({
dataSource={top5Services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, {
newTab: isModifierKeyPressed(event),
});
},
})}
/>

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { VerticalAlignTopOutlined } from '@ant-design/icons';
import { Button, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -11,15 +10,16 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import {
getFiltersFromParams,
getOrderByFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
useInfraMonitoringCurrentPage,
useInfraMonitoringFiltersHosts,
useInfraMonitoringOrderByHosts,
} from 'container/InfraMonitoringK8s/hooks';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Filter } from 'lucide-react';
import { parseAsString, useQueryState } from 'nuqs';
import { AppState } from 'store/reducers';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -35,50 +35,29 @@ function HostsList(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
);
if (!filters) {
return {
items: [],
op: 'and',
};
}
return filters;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filters, setFilters] = useInfraMonitoringFiltersHosts();
const [orderBy, setOrderBy] = useInfraMonitoringOrderByHosts();
const [showFilters, setShowFilters] = useState<boolean>(true);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams));
const [selectedHostName, setSelectedHostName] = useQueryState(
'hostName',
parseAsString.withDefault(''),
);
const handleOrderByChange = (
orderBy: {
orderByValue: {
columnName: string;
order: 'asc' | 'desc';
} | null,
): void => {
setOrderBy(orderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(orderBy),
});
setOrderBy(orderByValue);
};
const [selectedHostName, setSelectedHostName] = useState<string | null>(() => {
const hostName = searchParams.get('hostName');
return hostName || null;
});
const handleHostClick = (hostName: string): void => {
setSelectedHostName(hostName);
setSearchParams({ ...searchParams, hostName });
};
const { pageSize, setPageSize } = usePageSize('hosts');
@@ -154,12 +133,8 @@ function HostsList(): JSX.Element {
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
const isNewFilterAdded = value?.items?.length !== filters?.items?.length;
setFilters(value);
setFilters(value ?? null);
handleChangeQueryData('filters', value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value),
});
if (isNewFilterAdded) {
setCurrentPage(1);
@@ -171,8 +146,7 @@ function HostsList(): JSX.Element {
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[filters],
[filters, setFilters, setCurrentPage, handleChangeQueryData],
);
useEffect(() => {
@@ -184,7 +158,7 @@ function HostsList(): JSX.Element {
}, [data?.payload?.data?.total]);
const selectedHostData = useMemo(() => {
if (!selectedHostName) {
if (!selectedHostName?.trim()) {
return null;
}
return (
@@ -260,6 +234,7 @@ function HostsList(): JSX.Element {
pageSize={pageSize}
setPageSize={setPageSize}
setOrderBy={handleOrderByChange}
orderBy={orderBy}
/>
</div>
</div>

View File

@@ -14,7 +14,7 @@ function HostsListControls({
showAutoRefresh,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
filters: IBuilderQuery['filters'] | null;
showAutoRefresh: boolean;
}): JSX.Element {
const currentQuery = initialQueriesMap[DataSource.METRICS];

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import {
Skeleton,
@@ -11,6 +11,8 @@ import {
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
import {
@@ -114,8 +116,12 @@ export default function HostsListTable({
pageSize,
setOrderBy,
setPageSize,
orderBy,
}: HostsListTableProps): JSX.Element {
const columns = useMemo(() => getHostsListColumns(), []);
const [defaultOrderBy] = useState(orderBy);
const columns = useMemo(() => getHostsListColumns(defaultOrderBy), [
defaultOrderBy,
]);
const sentAnyHostMetricsData = useMemo(
() => data?.payload?.data?.sentAnyHostMetricsData || false,
@@ -162,7 +168,16 @@ export default function HostsListTable({
[],
);
const handleRowClick = (record: HostRowData): void => {
const handleRowClick = (
record: HostRowData,
event: React.MouseEvent,
): void => {
if (isModifierKeyPressed(event)) {
const params = new URLSearchParams(window.location.search);
params.set('hostName', record.hostName);
openInNewTab(`${window.location.pathname}?${params.toString()}`);
return;
}
onHostClick(record.hostName);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.HostEntity,
@@ -235,8 +250,8 @@ export default function HostsListTable({
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record: HostRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
/>

View File

@@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import * as appContextHooks from 'providers/App/App';
import * as timezoneHooks from 'providers/Timezone';
import store from 'store';
@@ -19,6 +20,16 @@ jest.mock('lib/getMinMax', () => ({
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
})),
}));
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: (): JSX.Element => (
<div data-testid="date-time-selection">Date Time</div>
),
}));
jest.mock('components/HostMetricsDetail', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
__esModule: true,
default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => (
@@ -55,19 +66,6 @@ jest.mock('react-router-dom', () => {
}),
};
});
jest.mock('react-router-dom-v5-compat', () => {
const actual = jest.requireActual('react-router-dom-v5-compat');
return {
...actual,
useSearchParams: jest
.fn()
.mockReturnValue([
{ get: jest.fn(), entries: jest.fn().mockReturnValue([]) },
jest.fn(),
]),
useNavigationType: (): any => 'PUSH',
};
});
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
@@ -127,29 +125,35 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
},
} as any);
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('HostsList', () => {
it('renders hosts list table', () => {
const { container } = render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<HostsList />
</Provider>
</MemoryRouter>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<HostsList />
</Provider>
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
});
it('renders filters', () => {
const { container } = render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<HostsList />
</Provider>
</MemoryRouter>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<HostsList />
</Provider>
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
expect(container.querySelector('.filters')).toBeInTheDocument();
});

View File

@@ -72,6 +72,7 @@ describe('HostsListTable', () => {
pageSize: 10,
setOrderBy: mockSetOrderBy,
setPageSize: mockSetPageSize,
orderBy: null,
};
it('renders loading state if isLoading is true and tableData is empty', () => {

View File

@@ -3,6 +3,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { SortOrder } from 'antd/lib/table/interface';
import {
HostData,
HostListPayload,
@@ -20,6 +21,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { OrderBySchemaType } from '../InfraMonitoringK8s/schemas';
import HostsList from './HostsList';
import './InfraMonitoring.styles.scss';
@@ -105,6 +107,7 @@ export interface HostsListTableProps {
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
) => void;
setPageSize: (pageSize: number) => void;
orderBy: OrderBySchemaType;
}
export interface EmptyOrLoadingViewProps {
@@ -127,6 +130,17 @@ export const getHostListsQuery = (): HostListPayload => ({
orderBy: { columnName: 'cpu', order: 'desc' },
});
function mapOrderByToSortOrder(
column: string,
orderBy: OrderBySchemaType,
): SortOrder | undefined {
return orderBy?.columnName === column
? orderBy?.order === 'asc'
? 'ascend'
: 'descend'
: undefined;
}
export const getTabsItems = (): TabsProps['items'] => [
{
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
@@ -135,7 +149,9 @@ export const getTabsItems = (): TabsProps['items'] => [
},
];
export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
export const getHostsListColumns = (
orderBy: OrderBySchemaType,
): ColumnType<HostRowData>[] => [
{
title: <div className="hostname-column-header">Hostname</div>,
dataIndex: 'hostName',
@@ -164,6 +180,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
key: 'cpu',
width: 100,
sorter: true,
defaultSortOrder: mapOrderByToSortOrder('cpu', orderBy),
align: 'right',
},
{
@@ -179,6 +196,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
key: 'memory',
width: 100,
sorter: true,
defaultSortOrder: mapOrderByToSortOrder('memory', orderBy),
align: 'right',
},
{
@@ -187,6 +205,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
key: 'wait',
width: 100,
sorter: true,
defaultSortOrder: mapOrderByToSortOrder('wait', orderBy),
align: 'right',
},
{
@@ -195,6 +214,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
key: 'load15',
width: 100,
sorter: true,
defaultSortOrder: mapOrderByToSortOrder('load15', orderBy),
align: 'right',
},
];

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -15,15 +14,15 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -93,23 +92,21 @@ function ClusterDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -129,15 +126,16 @@ function ClusterDetails({
},
],
};
}, [cluster?.meta.k8s_cluster_name, searchParams]);
}, [
cluster?.meta.k8s_cluster_name,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -166,7 +164,7 @@ function ClusterDetails({
},
],
};
}, [cluster?.meta.k8s_cluster_name, searchParams]);
}, [cluster?.meta.k8s_cluster_name, eventsFiltersParam]);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -207,13 +205,9 @@ function ClusterDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -287,13 +281,8 @@ function ClusterDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -330,13 +319,8 @@ function ClusterDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -379,13 +363,8 @@ function ClusterDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -24,15 +23,22 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringClusterName,
useInfraMonitoringCurrentPage,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -47,6 +53,7 @@ import {
import '../InfraMonitoringK8s.styles.scss';
import './K8sClustersList.styles.scss';
function K8sClustersList({
isFiltersVisible,
handleFilterVisibilityChange,
@@ -60,55 +67,19 @@ function K8sClustersList({
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [
selectedClusterName,
setselectedClusterName,
] = useInfraMonitoringClusterName();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [filtersInitialised, setFiltersInitialised] = useState(false);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, false));
const [selectedClusterName, setselectedClusterName] = useState<string | null>(
() => {
const clusterName = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
);
if (clusterName) {
return clusterName;
}
return null;
},
);
const { pageSize, setPageSize } = usePageSize(K8sCategory.CLUSTERS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [
selectedRowData,
setSelectedRowData,
@@ -134,7 +105,7 @@ function K8sClustersList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -187,25 +158,28 @@ function K8sClustersList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedClusterName) {
return [
'clusterList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'clusterList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -264,7 +238,7 @@ function K8sClustersList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -372,26 +346,15 @@ function K8sClustersList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -450,14 +413,31 @@ function K8sClustersList({
);
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
const handleRowClick = (record: K8sClustersRowData): void => {
const openClusterInNewTab = (record: K8sClustersRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
record.clusterUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sClustersRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openClusterInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME]: record.clusterUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -486,11 +466,6 @@ function K8sClustersList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -514,8 +489,14 @@ function K8sClustersList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
openClusterInNewTab(record);
return;
}
setselectedClusterName(record.clusterUID);
},
className: 'expanded-clickable-row',
@@ -581,25 +562,11 @@ function K8sClustersList({
const handleCloseClusterDetail = (): void => {
setselectedClusterName(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
};
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
const newGroupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
@@ -609,17 +576,13 @@ function K8sClustersList({
);
if (key) {
groupBy.push(key);
newGroupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setGroupBy(newGroupBy);
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -627,7 +590,7 @@ function K8sClustersList({
category: InfraMonitoringEvents.Cluster,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -706,8 +669,10 @@ function K8sClustersList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sClustersData,
K8sClustersListPayload,

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -15,11 +14,14 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -93,23 +95,21 @@ function DaemonSetDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -143,16 +143,14 @@ function DaemonSetDetails({
}, [
daemonSet?.meta.k8s_daemonset_name,
daemonSet?.meta.k8s_namespace_name,
searchParams,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -181,7 +179,7 @@ function DaemonSetDetails({
},
],
};
}, [daemonSet?.meta.k8s_daemonset_name, searchParams]);
}, [daemonSet?.meta.k8s_daemonset_name, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -222,13 +220,9 @@ function DaemonSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -296,20 +290,17 @@ function DaemonSetDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
},
@@ -337,21 +328,18 @@ function DaemonSetDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -392,13 +380,8 @@ function DaemonSetDetails({
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -25,15 +24,26 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringDaemonSetUID,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringOrderBy,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -62,54 +72,25 @@ function K8sDaemonSetsList({
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedDaemonSetUID, setSelectedDaemonSetUID] = useState<
string | null
>(() => {
const daemonSetUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
);
if (daemonSetUID) {
return daemonSetUID;
}
return null;
});
const [
selectedDaemonSetUID,
setSelectedDaemonSetUID,
] = useInfraMonitoringDaemonSetUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.DAEMONSETS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [
selectedRowData,
@@ -136,7 +117,7 @@ function K8sDaemonSetsList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -189,25 +170,28 @@ function K8sDaemonSetsList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedDaemonSetUID) {
return [
'daemonSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'daemonSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -266,7 +250,7 @@ function K8sDaemonSetsList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -376,26 +360,15 @@ function K8sDaemonSetsList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -456,14 +429,31 @@ function K8sDaemonSetsList({
nestedDaemonSetsData,
]);
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
const openDaemonSetInNewTab = (record: K8sDaemonSetsRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
record.daemonsetUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sDaemonSetsRowData,
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openDaemonSetInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedDaemonSetUID(record.daemonsetUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID]: record.daemonsetUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -492,11 +482,6 @@ function K8sDaemonSetsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -520,8 +505,14 @@ function K8sDaemonSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
openDaemonSetInNewTab(record);
return;
}
setSelectedDaemonSetUID(record.daemonsetUID);
},
className: 'expanded-clickable-row',
@@ -587,20 +578,10 @@ function K8sDaemonSetsList({
const handleCloseDaemonSetDetail = (): void => {
setSelectedDaemonSetUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleGroupByChange = useCallback(
@@ -621,10 +602,6 @@ function K8sDaemonSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
@@ -633,7 +610,7 @@ function K8sDaemonSetsList({
category: InfraMonitoringEvents.DaemonSet,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -714,8 +691,10 @@ function K8sDaemonSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sDaemonSetsData,
K8sDaemonSetsListPayload,

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -16,15 +15,15 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -97,23 +96,21 @@ function DeploymentDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -147,16 +144,14 @@ function DeploymentDetails({
}, [
deployment?.meta.k8s_deployment_name,
deployment?.meta.k8s_namespace_name,
searchParams,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -185,7 +180,7 @@ function DeploymentDetails({
},
],
};
}, [deployment?.meta.k8s_deployment_name, searchParams]);
}, [deployment?.meta.k8s_deployment_name, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -226,13 +221,9 @@ function DeploymentDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -309,13 +300,8 @@ function DeploymentDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -354,13 +340,8 @@ function DeploymentDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -403,13 +384,8 @@ function DeploymentDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -17,23 +16,34 @@ import logEvent from 'api/common/logEvent';
import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList';
import classNames from 'classnames';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sDeploymentsList } from 'hooks/infraMonitoring/useGetK8sDeploymentsList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringDeploymentUID,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringOrderBy,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -62,55 +72,26 @@ function K8sDeploymentsList({
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, true));
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [selectedDeploymentUID, setselectedDeploymentUID] = useState<
string | null
>(() => {
const deploymentUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
);
if (deploymentUID) {
return deploymentUID;
}
return null;
});
const [
selectedDeploymentUID,
setselectedDeploymentUID,
] = useInfraMonitoringDeploymentUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.DEPLOYMENTS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [
selectedRowData,
@@ -137,7 +118,7 @@ function K8sDeploymentsList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -190,25 +171,28 @@ function K8sDeploymentsList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedDeploymentUID) {
return [
'deploymentList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'deploymentList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -267,7 +251,7 @@ function K8sDeploymentsList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -379,26 +363,15 @@ function K8sDeploymentsList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -462,14 +435,31 @@ function K8sDeploymentsList({
nestedDeploymentsData,
]);
const handleRowClick = (record: K8sDeploymentsRowData): void => {
const openDeploymentInNewTab = (record: K8sDeploymentsRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
record.deploymentUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sDeploymentsRowData,
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openDeploymentInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID]: record.deploymentUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -498,11 +488,6 @@ function K8sDeploymentsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -526,8 +511,14 @@ function K8sDeploymentsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
openDeploymentInNewTab(record);
return;
}
setselectedDeploymentUID(record.deploymentUID);
},
className: 'expanded-clickable-row',
@@ -593,20 +584,10 @@ function K8sDeploymentsList({
const handleCloseDeploymentDetail = (): void => {
setselectedDeploymentUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleGroupByChange = useCallback(
@@ -628,10 +609,6 @@ function K8sDeploymentsList({
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
@@ -640,7 +617,7 @@ function K8sDeploymentsList({
category: InfraMonitoringEvents.Deployment,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -721,8 +698,10 @@ function K8sDeploymentsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sDeploymentsData,
K8sDeploymentsListPayload,

View File

@@ -147,9 +147,11 @@
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}

View File

@@ -121,9 +121,11 @@
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}

View File

@@ -102,6 +102,7 @@
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
@@ -185,6 +186,7 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
@@ -369,9 +371,11 @@
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}

View File

@@ -1,5 +1,4 @@
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { VerticalAlignTopOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react';
import type { CollapseProps } from 'antd';
@@ -37,11 +36,16 @@ import {
GetPodsQuickFiltersConfig,
GetStatefulsetsQuickFiltersConfig,
GetVolumesQuickFiltersConfig,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategories,
} from './constants';
import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
import {
useInfraMonitoringCategory,
useInfraMonitoringFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
} from './hooks';
import K8sJobsList from './Jobs/K8sJobsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
import K8sNodesList from './Nodes/K8sNodesList';
@@ -54,14 +58,11 @@ import './InfraMonitoringK8s.styles.scss';
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedCategory, setSelectedCategory] = useState(() => {
const category = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY);
if (category) {
return category as keyof typeof K8sCategories;
}
return K8sCategories.PODS;
});
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [, setFilters] = useInfraMonitoringFilters();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setOrderBy] = useInfraMonitoringOrderBy();
const [quickFiltersLastUpdated, setQuickFiltersLastUpdated] = useState(-1);
const { currentQuery } = useQueryBuilder();
@@ -86,12 +87,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
handleChangeQueryData('filters', query.builder.queryData[0].filters);
setQuickFiltersLastUpdated(Date.now());
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(
query.builder.queryData[0].filters,
),
});
setFilters(JSON.stringify(query.builder.queryData[0].filters));
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -317,10 +313,10 @@ export default function InfraMonitoringK8s(): JSX.Element {
const handleCategoryChange = (key: string | string[]): void => {
if (Array.isArray(key) && key.length > 0) {
setSelectedCategory(key[0] as string);
setSearchParams({
[INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY]: key[0] as string,
});
// Reset filters
setFilters(null);
setOrderBy(null);
setGroupBy(null);
handleChangeQueryData('filters', { items: [], op: 'and' });
}
};

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -15,11 +14,14 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -90,23 +92,21 @@ function JobDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -137,15 +137,17 @@ function JobDetails({
},
],
};
}, [job?.meta.k8s_job_name, job?.meta.k8s_namespace_name, searchParams]);
}, [
job?.meta.k8s_job_name,
job?.meta.k8s_namespace_name,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -174,7 +176,7 @@ function JobDetails({
},
],
};
}, [job?.meta.k8s_job_name, searchParams]);
}, [job?.meta.k8s_job_name, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -215,13 +217,9 @@ function JobDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -288,20 +286,17 @@ function JobDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -330,21 +325,18 @@ function JobDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -385,13 +377,8 @@ function JobDetails({
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -17,23 +16,34 @@ import logEvent from 'api/common/logEvent';
import { K8sJobsListPayload } from 'api/infraMonitoring/getK8sJobsList';
import classNames from 'classnames';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sJobsList } from 'hooks/infraMonitoring/useGetK8sJobsList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringJobUID,
useInfraMonitoringLogFilters,
useInfraMonitoringOrderBy,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -61,51 +71,24 @@ function K8sJobsList({
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, true));
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [selectedJobUID, setselectedJobUID] = useState<string | null>(() => {
const jobUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID);
if (jobUID) {
return jobUID;
}
return null;
});
const [selectedJobUID, setselectedJobUID] = useInfraMonitoringJobUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.JOBS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [selectedRowData, setSelectedRowData] = useState<K8sJobsRowData | null>(
null,
@@ -131,7 +114,7 @@ function K8sJobsList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -184,25 +167,28 @@ function K8sJobsList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedJobUID) {
return [
'jobList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'jobList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -254,7 +240,7 @@ function K8sJobsList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -360,26 +346,15 @@ function K8sJobsList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -427,14 +402,28 @@ function K8sJobsList({
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
const handleRowClick = (record: K8sJobsRowData): void => {
const openJobInNewTab = (record: K8sJobsRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sJobsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openJobInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedJobUID(record.jobUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID]: record.jobUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -463,11 +452,6 @@ function K8sJobsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -491,8 +475,14 @@ function K8sJobsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
openJobInNewTab(record);
return;
}
setselectedJobUID(record.jobUID);
},
className: 'expanded-clickable-row',
@@ -558,20 +548,10 @@ function K8sJobsList({
const handleCloseJobDetail = (): void => {
setselectedJobUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleGroupByChange = useCallback(
@@ -593,10 +573,6 @@ function K8sJobsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -604,7 +580,7 @@ function K8sJobsList({
category: InfraMonitoringEvents.Job,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -683,8 +659,10 @@ function K8sJobsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sJobsData,
K8sJobsListPayload,

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Button, Select } from 'antd';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
@@ -10,7 +9,8 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
import { K8sCategory } from './constants';
import { useInfraMonitoringFiltersK8s } from './hooks';
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
import { IEntityColumn } from './utils';
@@ -50,17 +50,14 @@ function K8sHeader({
showAutoRefresh,
}: K8sHeaderProps): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const currentQuery = initialQueriesMap[DataSource.METRICS];
const updatedCurrentQuery = useMemo(() => {
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
const decoded = decodeURIComponent(urlFilters);
const parsed = JSON.parse(decoded);
filters = parsed;
filters = urlFilters;
}
return {
...currentQuery,
@@ -78,19 +75,16 @@ function K8sHeader({
],
},
};
}, [currentQuery, searchParams]);
}, [currentQuery, urlFilters]);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleFiltersChange(value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value),
});
setUrlFilters(value || null);
},
[handleFiltersChange, searchParams, setSearchParams],
[handleFiltersChange, setUrlFilters],
);
return (

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -16,23 +15,34 @@ import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sNamespacesListPayload } from 'api/infraMonitoring/getK8sNamespacesList';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sNamespacesList } from 'hooks/infraMonitoring/useGetK8sNamespacesList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringNamespaceUID,
useInfraMonitoringOrderBy,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -62,53 +72,25 @@ function K8sNamespacesList({
);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, true));
const [selectedNamespaceUID, setselectedNamespaceUID] = useState<
string | null
>(() => {
const namespaceUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
);
if (namespaceUID) {
return namespaceUID;
}
return null;
});
const [
selectedNamespaceUID,
setselectedNamespaceUID,
] = useInfraMonitoringNamespaceUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.NAMESPACES);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [
selectedRowData,
@@ -135,7 +117,7 @@ function K8sNamespacesList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -188,25 +170,28 @@ function K8sNamespacesList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedNamespaceUID) {
return [
'namespaceList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'namespaceList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -265,7 +250,7 @@ function K8sNamespacesList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -375,26 +360,15 @@ function K8sNamespacesList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -458,14 +432,31 @@ function K8sNamespacesList({
nestedNamespacesData,
]);
const handleRowClick = (record: K8sNamespacesRowData): void => {
const openNamespaceInNewTab = (record: K8sNamespacesRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
record.namespaceUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sNamespacesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openNamespaceInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID]: record.namespaceUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -494,11 +485,6 @@ function K8sNamespacesList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -522,8 +508,14 @@ function K8sNamespacesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
openNamespaceInNewTab(record);
return;
}
setselectedNamespaceUID(record.namespaceUID);
},
className: 'expanded-clickable-row',
@@ -589,20 +581,10 @@ function K8sNamespacesList({
const handleCloseNamespaceDetail = (): void => {
setselectedNamespaceUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleGroupByChange = useCallback(
@@ -625,10 +607,6 @@ function K8sNamespacesList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -636,7 +614,7 @@ function K8sNamespacesList({
category: InfraMonitoringEvents.Namespace,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -715,8 +693,10 @@ function K8sNamespacesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -101,6 +101,7 @@
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
@@ -184,6 +185,7 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -16,12 +15,15 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -94,23 +96,21 @@ function NamespaceDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -130,15 +130,16 @@ function NamespaceDetails({
},
],
};
}, [namespace?.namespaceName, searchParams]);
}, [
namespace?.namespaceName,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -167,7 +168,7 @@ function NamespaceDetails({
},
],
};
}, [namespace?.namespaceName, searchParams]);
}, [namespace?.namespaceName, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -208,13 +209,9 @@ function NamespaceDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -281,21 +278,17 @@ function NamespaceDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -324,21 +317,18 @@ function NamespaceDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -379,13 +369,8 @@ function NamespaceDetails({
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag } from 'antd';
import {
K8sNamespacesData,
K8sNamespacesListPayload,

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -16,23 +15,34 @@ import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sNodesListPayload } from 'api/infraMonitoring/getK8sNodesList';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sNodesList } from 'hooks/infraMonitoring/useGetK8sNodesList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringNodeUID,
useInfraMonitoringOrderBy,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -60,50 +70,24 @@ function K8sNodesList({
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, false));
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [selectedNodeUID, setSelectedNodeUID] = useState<string | null>(() => {
const nodeUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID);
if (nodeUID) {
return nodeUID;
}
return null;
});
const [selectedNodeUID, setSelectedNodeUID] = useInfraMonitoringNodeUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.NODES);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
// These params are used only for clearing in handleCloseNodeDetail
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [selectedRowData, setSelectedRowData] = useState<K8sNodesRowData | null>(
null,
@@ -129,7 +113,7 @@ function K8sNodesList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -182,25 +166,28 @@ function K8sNodesList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedNodeUID) {
return [
'nodeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'nodeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -259,7 +246,7 @@ function K8sNodesList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -365,26 +352,15 @@ function K8sNodesList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -437,14 +413,28 @@ function K8sNodesList({
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
const handleRowClick = (record: K8sNodesRowData): void => {
const openNodeInNewTab = (record: K8sNodesRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sNodesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openNodeInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedNodeUID(record.nodeUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID]: record.nodeUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -473,11 +463,6 @@ function K8sNodesList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -502,8 +487,14 @@ function K8sNodesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
openNodeInNewTab(record);
return;
}
setSelectedNodeUID(record.nodeUID);
},
className: 'expanded-clickable-row',
@@ -569,45 +560,31 @@ function K8sNodesList({
const handleCloseNodeDetail = (): void => {
setSelectedNodeUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
const newGroupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
(k) => k.key === element,
);
if (key) {
groupBy.push(key);
newGroupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setGroupBy(newGroupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -615,7 +592,7 @@ function K8sNodesList({
category: InfraMonitoringEvents.Node,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -694,8 +671,10 @@ function K8sNodesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -16,15 +15,15 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import NodeEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -94,23 +93,21 @@ function NodeDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -130,15 +127,16 @@ function NodeDetails({
},
],
};
}, [node?.meta.k8s_node_name, searchParams]);
}, [
node?.meta.k8s_node_name,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -167,7 +165,7 @@ function NodeDetails({
},
],
};
}, [node?.meta.k8s_node_name, searchParams]);
}, [node?.meta.k8s_node_name, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -208,13 +206,9 @@ function NodeDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -290,13 +284,8 @@ function NodeDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -335,13 +324,8 @@ function NodeDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -382,13 +366,8 @@ function NodeDetails({
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sNodesData,
K8sNodesListPayload,

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -19,23 +18,34 @@ import logEvent from 'api/common/logEvent';
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
import classNames from 'classnames';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringOrderBy,
useInfraMonitoringPodUID,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import {
@@ -64,41 +74,25 @@ function K8sPodsList({
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [defaultOrderBy] = useState(orderBy);
const [selectedPodUID, setSelectedPodUID] = useInfraMonitoringPodUID();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [addedColumns, setAddedColumns] = useState<IEntityColumn[]>([]);
const [availableColumns, setAvailableColumns] = useState<IEntityColumn[]>(
defaultAvailableColumns,
);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [selectedRowData, setSelectedRowData] = useState<K8sPodsRowData | null>(
null,
);
@@ -151,7 +145,7 @@ function K8sPodsList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
useEffect(() => {
const addedColumns = JSON.parse(get('k8sPodsAddedColumns') ?? '[]');
@@ -170,19 +164,6 @@ function K8sPodsList({
}
}, []);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, false));
const [selectedPodUID, setSelectedPodUID] = useState<string | null>(() => {
const podUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID);
if (podUID) {
return podUID;
}
return null;
});
const { pageSize, setPageSize } = usePageSize(K8sCategory.PODS);
const query = useMemo(() => {
@@ -195,7 +176,7 @@ function K8sPodsList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
@@ -293,25 +274,28 @@ function K8sPodsList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedPodUID) {
return [
'podList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'podList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -354,10 +338,10 @@ function K8sPodsList({
[groupedByRowData, groupBy],
);
const columns = useMemo(() => getK8sPodsListColumns(addedColumns, groupBy), [
addedColumns,
groupBy,
]);
const columns = useMemo(
() => getK8sPodsListColumns(addedColumns, groupBy, defaultOrderBy),
[addedColumns, groupBy, defaultOrderBy],
);
const handleTableChange: TableProps<K8sPodsRowData>['onChange'] = useCallback(
(
@@ -375,26 +359,15 @@ function K8sPodsList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -426,28 +399,24 @@ function K8sPodsList({
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
const newGroupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
(k) => k.key === element,
);
if (key) {
groupBy.push(key);
newGroupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setGroupBy(newGroupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -455,7 +424,7 @@ function K8sPodsList({
category: InfraMonitoringEvents.Pod,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -495,13 +464,27 @@ function K8sPodsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const handleRowClick = (record: K8sPodsRowData): void => {
const openPodInNewTab = (record: K8sPodsRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sPodsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openPodInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedPodUID(record.podUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID]: record.podUID,
});
setSelectedRowData(null);
} else {
handleGroupByRowClick(record);
@@ -516,20 +499,10 @@ function K8sPodsList({
const handleClosePodDetail = (): void => {
setSelectedPodUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleAddColumn = useCallback(
@@ -568,9 +541,10 @@ function K8sPodsList({
[setAddedColumns, setAvailableColumns],
);
const nestedColumns = useMemo(() => getK8sPodsListColumns(addedColumns, []), [
addedColumns,
]);
const nestedColumns = useMemo(
() => getK8sPodsListColumns(addedColumns, [], defaultOrderBy),
[addedColumns, defaultOrderBy],
);
const isGroupedByAttribute = groupBy.length > 0;
@@ -587,11 +561,6 @@ function K8sPodsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -615,8 +584,14 @@ function K8sPodsList({
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record: K8sPodsRowData,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
openPodInNewTab(record);
return;
}
setSelectedPodUID(record.podUID);
},
className: 'expanded-clickable-row',
@@ -752,8 +727,10 @@ function K8sPodsList({
scroll={{ x: true }}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record: K8sPodsRowData,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -16,15 +15,15 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
filterDuplicateFilters,
getFiltersFromParams,
} from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -96,23 +95,21 @@ function PodDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -143,15 +140,17 @@ function PodDetails({
},
],
};
}, [pod?.meta.k8s_namespace_name, pod?.meta.k8s_pod_name, searchParams]);
}, [
pod?.meta.k8s_namespace_name,
pod?.meta.k8s_pod_name,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -180,7 +179,7 @@ function PodDetails({
},
],
};
}, [pod?.meta.k8s_pod_name, searchParams]);
}, [pod?.meta.k8s_pod_name, eventsFiltersParam]);
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -221,13 +220,9 @@ function PodDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -305,13 +300,8 @@ function PodDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -352,13 +342,8 @@ function PodDetails({
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -399,13 +384,8 @@ function PodDetails({
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -17,23 +16,34 @@ import logEvent from 'api/common/logEvent';
import { K8sStatefulSetsListPayload } from 'api/infraMonitoring/getsK8sStatefulSetsList';
import classNames from 'classnames';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sStatefulSetsList } from 'hooks/infraMonitoring/useGetK8sStatefulSetsList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringOrderBy,
useInfraMonitoringStatefulSetUID,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -48,6 +58,7 @@ import {
import '../InfraMonitoringK8s.styles.scss';
import './K8sStatefulSetsList.styles.scss';
function K8sStatefulSetsList({
isFiltersVisible,
handleFilterVisibilityChange,
@@ -61,55 +72,26 @@ function K8sStatefulSetsList({
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, true));
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [selectedStatefulSetUID, setselectedStatefulSetUID] = useState<
string | null
>(() => {
const statefulSetUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
);
if (statefulSetUID) {
return statefulSetUID;
}
return null;
});
const [
selectedStatefulSetUID,
setselectedStatefulSetUID,
] = useInfraMonitoringStatefulSetUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.STATEFULSETS);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [
selectedRowData,
@@ -136,7 +118,7 @@ function K8sStatefulSetsList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -189,25 +171,28 @@ function K8sStatefulSetsList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
// be careful with what you serialize from selectedRowData
// since it's react node, it could contain circular references
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
if (selectedStatefulSetUID) {
return [
'statefulSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
];
}
return [
'statefulSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
@@ -266,7 +251,7 @@ function K8sStatefulSetsList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -378,26 +363,15 @@ function K8sStatefulSetsList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -459,14 +433,31 @@ function K8sStatefulSetsList({
nestedStatefulSetsData,
]);
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
const openStatefulSetInNewTab = (record: K8sStatefulSetsRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
record.statefulsetUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sStatefulSetsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openStatefulSetInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID]: record.statefulsetUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -495,11 +486,6 @@ function K8sStatefulSetsList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -523,8 +509,14 @@ function K8sStatefulSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
openStatefulSetInNewTab(record);
return;
}
setselectedStatefulSetUID(record.statefulsetUID);
},
className: 'expanded-clickable-row',
@@ -590,20 +582,10 @@ function K8sStatefulSetsList({
const handleCloseStatefulSetDetail = (): void => {
setselectedStatefulSetUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) =>
![
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
].includes(key),
),
),
});
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(null);
};
const handleGroupByChange = useCallback(
@@ -625,10 +607,6 @@ function K8sStatefulSetsList({
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
@@ -636,7 +614,7 @@ function K8sStatefulSetsList({
category: InfraMonitoringEvents.StatefulSet,
});
},
[groupByFiltersData, searchParams, setSearchParams],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -717,8 +695,10 @@ function K8sStatefulSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
@@ -15,16 +14,19 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from 'container/InfraMonitoringK8s/constants';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import EntityEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents';
import EntityLogs from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs';
import EntityMetrics from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityMetrics';
import EntityTraces from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces';
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from 'container/InfraMonitoringK8s/hooks';
import {
CustomTimeType,
Time,
@@ -93,23 +95,21 @@ function StatefulSetDetails({
: (selectedTime as Time),
);
const [searchParams, setSearchParams] = useSearchParams();
const [selectedView, setSelectedView] = useState<VIEWS>(() => {
const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
if (view) {
return view as VIEWS;
}
return VIEWS.METRICS;
});
const [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
@@ -141,18 +141,16 @@ function StatefulSetDetails({
],
};
}, [
searchParams,
statefulSet?.meta.k8s_statefulset_name,
statefulSet?.meta.k8s_namespace_name,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
const filters = getFiltersFromParams(
searchParams,
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
);
if (filters) {
return filters;
if (eventsFiltersParam) {
return eventsFiltersParam;
}
return {
op: 'AND',
@@ -181,7 +179,7 @@ function StatefulSetDetails({
},
],
};
}, [searchParams, statefulSet?.meta.k8s_statefulset_name]);
}, [statefulSet?.meta.k8s_statefulset_name, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -222,13 +220,9 @@ function StatefulSetDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null),
});
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
@@ -296,20 +290,17 @@ function StatefulSetDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -338,21 +329,18 @@ function StatefulSetDetails({
const updatedFilters = {
op: 'AND',
items: [
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
@@ -393,13 +381,8 @@ function StatefulSetDetails({
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
setEventsFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sStatefulSetsData,
K8sStatefulSetsListPayload,

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
@@ -17,23 +16,30 @@ import logEvent from 'api/common/logEvent';
import { K8sVolumesListPayload } from 'api/infraMonitoring/getK8sVolumesList';
import classNames from 'classnames';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sVolumesList } from 'hooks/infraMonitoring/useGetK8sVolumesList';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringVolumeUID,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -48,6 +54,7 @@ import VolumeDetails from './VolumeDetails';
import '../InfraMonitoringK8s.styles.scss';
import './K8sVolumesList.styles.scss';
function K8sVolumesList({
isFiltersVisible,
handleFilterVisibilityChange,
@@ -61,55 +68,21 @@ function K8sVolumesList({
(state) => state.globalTime,
);
const [searchParams, setSearchParams] = useSearchParams();
const [currentPage, setCurrentPage] = useState(() => {
const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE);
if (page) {
return parseInt(page, 10);
}
return 1;
});
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
useEffect(() => {
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(() => getOrderByFromParams(searchParams, true));
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [selectedVolumeUID, setselectedVolumeUID] = useState<string | null>(
() => {
const volumeUID = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
);
if (volumeUID) {
return volumeUID;
}
return null;
},
);
const [
selectedVolumeUID,
setselectedVolumeUID,
] = useInfraMonitoringVolumeUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.VOLUMES);
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
}
return [];
});
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [
selectedRowData,
@@ -136,7 +109,7 @@ function K8sVolumesList({
if (quickFiltersLastUpdated !== -1) {
setCurrentPage(1);
}
}, [quickFiltersLastUpdated]);
}, [quickFiltersLastUpdated, setCurrentPage]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
@@ -189,7 +162,7 @@ function K8sVolumesList({
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
@@ -240,7 +213,7 @@ function K8sVolumesList({
filters: queryFilters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
orderBy: orderBy || baseQuery.orderBy,
};
if (groupBy.length > 0) {
queryPayload.groupBy = groupBy;
@@ -313,26 +286,15 @@ function K8sVolumesList({
}
if ('field' in sorter && sorter.order) {
const currentOrderBy = {
setOrderBy({
columnName: sorter.field as string,
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
};
setOrderBy(currentOrderBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(
currentOrderBy,
),
});
} else {
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
}
},
[searchParams, setSearchParams],
[setCurrentPage, setOrderBy],
);
const { handleChangeQueryData } = useQueryOperations({
@@ -389,14 +351,28 @@ function K8sVolumesList({
);
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
const handleRowClick = (record: K8sVolumesRowData): void => {
const openVolumeInNewTab = (record: K8sVolumesRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sVolumesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openVolumeInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedVolumeUID(record.volumeUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID]: record.volumeUID,
});
} else {
handleGroupByRowClick(record);
}
@@ -425,11 +401,6 @@ function K8sVolumesList({
setSelectedRowData(null);
setGroupBy([]);
setOrderBy(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]),
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null),
});
};
const expandedRowRender = (): JSX.Element => (
@@ -453,8 +424,14 @@ function K8sVolumesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
openVolumeInNewTab(record);
return;
}
setselectedVolumeUID(record.volumeUID);
},
className: 'expanded-clickable-row',
@@ -520,13 +497,6 @@ function K8sVolumesList({
const handleCloseVolumeDetail = (): void => {
setselectedVolumeUID(null);
setSearchParams({
...Object.fromEntries(
Array.from(searchParams.entries()).filter(
([key]) => key !== INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
),
),
});
};
const handleGroupByChange = useCallback(
@@ -547,10 +517,6 @@ function K8sVolumesList({
setCurrentPage(1);
setGroupBy(groupBy);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy),
});
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
@@ -559,7 +525,7 @@ function K8sVolumesList({
category: InfraMonitoringEvents.Volumes,
});
},
[groupByFiltersData?.payload?.attributeKeys, searchParams, setSearchParams],
[groupByFiltersData?.payload?.attributeKeys, setCurrentPage, setGroupBy],
);
useEffect(() => {
@@ -640,8 +606,10 @@ function K8sVolumesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sVolumesData,
K8sVolumesListPayload,
@@ -75,7 +74,7 @@ export const getK8sVolumesListQuery = (): K8sVolumesListPayload => ({
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
orderBy: { columnName: 'usage', order: 'desc' },
});
const columnsConfig = [

View File

@@ -8,10 +8,13 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import ClusterDetails from 'container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('ClusterDetails', () => {
const mockCluster = {
meta: {
@@ -22,17 +25,19 @@ describe('ClusterDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const clusterNameElements = screen.getAllByText('test-cluster');
@@ -42,17 +47,19 @@ describe('ClusterDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -70,17 +77,19 @@ describe('ClusterDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -89,17 +98,19 @@ describe('ClusterDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -110,17 +121,19 @@ describe('ClusterDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<ClusterDetails
cluster={mockCluster}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -8,10 +8,13 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import DaemonSetDetails from 'container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('DaemonSetDetails', () => {
const mockDaemonSet = {
meta: {
@@ -24,17 +27,19 @@ describe('DaemonSetDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const daemonSetNameElements = screen.getAllByText('test-daemon-set');
@@ -52,17 +57,19 @@ describe('DaemonSetDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -80,17 +87,19 @@ describe('DaemonSetDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -99,17 +108,19 @@ describe('DaemonSetDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -120,17 +131,19 @@ describe('DaemonSetDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DaemonSetDetails
daemonSet={mockDaemonSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -8,10 +8,13 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import DeploymentDetails from 'container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('DeploymentDetails', () => {
const mockDeployment = {
meta: {
@@ -24,17 +27,19 @@ describe('DeploymentDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const deploymentNameElements = screen.getAllByText('test-deployment');
@@ -52,17 +57,19 @@ describe('DeploymentDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -80,17 +87,19 @@ describe('DeploymentDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -99,17 +108,19 @@ describe('DeploymentDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -120,17 +131,19 @@ describe('DeploymentDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<DeploymentDetails
deployment={mockDeployment}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -2,8 +2,18 @@ import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
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 } from '@testing-library/react';
import JobDetails from 'container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails';
import { fireEvent, render, screen } from 'tests/test-utils';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('JobDetails', () => {
const mockJob = {
@@ -16,7 +26,15 @@ describe('JobDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const jobNameElements = screen.getAllByText('test-job');
@@ -30,7 +48,15 @@ describe('JobDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -48,7 +74,15 @@ describe('JobDetails', () => {
it('default tab should be metrics', () => {
render(
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -57,7 +91,15 @@ describe('JobDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -68,7 +110,15 @@ describe('JobDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<JobDetails job={mockJob} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -0,0 +1,131 @@
import setupCommonMocks from './commonMocks';
setupCommonMocks();
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import K8sHeader from 'container/InfraMonitoringK8s/K8sHeader';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from '../constants';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
describe('K8sHeader URL Parameter Parsing', () => {
const defaultProps = {
selectedGroupBy: [],
groupByOptions: [],
isLoadingGroupByFilters: false,
handleFiltersChange: jest.fn(),
handleGroupByChange: jest.fn(),
defaultAddedColumns: [],
handleFilterVisibilityChange: jest.fn(),
isFiltersVisible: true,
entity: K8sCategory.PODS,
showAutoRefresh: true,
};
const renderComponent = (
searchParams?: string | Record<string, string>,
): ReturnType<typeof render> => {
const Wrapper = withNuqsTestingAdapter({ searchParams: searchParams ?? {} });
return render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sHeader {...defaultProps} />
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should render without crashing when no URL params', () => {
expect(() => renderComponent()).not.toThrow();
expect(screen.getByText('Group by')).toBeInTheDocument();
});
it('should render without crashing with valid filters in URL', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_namespace_name' },
op: '=',
value: 'kube-system',
},
],
op: 'AND',
};
expect(() =>
renderComponent({
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(filters),
}),
).not.toThrow();
});
it('should render without crashing with malformed filters JSON', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() =>
renderComponent({
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: 'invalid-json',
}),
).not.toThrow();
consoleSpy.mockRestore();
});
it('should handle filters with K8s container image values', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_container_image' },
op: '=',
value: 'registry.k8s.io/coredns/coredns:v1.10.1',
},
],
op: 'AND',
};
expect(() =>
renderComponent({
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(filters),
}),
).not.toThrow();
});
it('should handle filters with percent signs in values', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_label' },
op: '=',
value: 'cpu-usage-50%',
},
],
op: 'AND',
};
expect(() =>
renderComponent({
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(filters),
}),
).not.toThrow();
});
});

View File

@@ -8,10 +8,13 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import NamespaceDetails from 'container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('NamespaceDetails', () => {
const mockNamespace = {
namespaceName: 'test-namespace',
@@ -23,17 +26,19 @@ describe('NamespaceDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const namespaceNameElements = screen.getAllByText('test-namespace');
@@ -47,17 +52,19 @@ describe('NamespaceDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -75,17 +82,19 @@ describe('NamespaceDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -94,17 +103,19 @@ describe('NamespaceDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -115,17 +126,19 @@ describe('NamespaceDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -8,10 +8,13 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import NodeDetails from 'container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('NodeDetails', () => {
const mockNode = {
meta: {
@@ -23,13 +26,19 @@ describe('NodeDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails
node={mockNode}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const nodeNameElements = screen.getAllByText('test-node');
@@ -43,13 +52,19 @@ describe('NodeDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails
node={mockNode}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -67,13 +82,19 @@ describe('NodeDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails
node={mockNode}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -82,13 +103,19 @@ describe('NodeDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails
node={mockNode}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -99,13 +126,19 @@ describe('NodeDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails node={mockNode} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<NodeDetails
node={mockNode}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -0,0 +1,155 @@
/**
* Tests for URL parameter parsing in K8s Infra Monitoring components.
*
* These tests verify the fix for the double URL decoding bug where
* components were calling decodeURIComponent() on values already
* decoded by URLSearchParams.get(), causing crashes on K8s parameters
* with special characters.
*/
import { getFiltersFromParams } from '../../commonUtils';
describe('K8sPodsList URL Parameter Parsing', () => {
describe('getFiltersFromParams', () => {
it('should return null when no filters in params', () => {
const searchParams = new URLSearchParams();
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toBeNull();
});
it('should parse filters from URL params', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_namespace_name' },
op: '=',
value: 'default',
},
],
op: 'AND',
};
const searchParams = new URLSearchParams();
searchParams.set('filters', JSON.stringify(filters));
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toEqual(filters);
});
it('should handle URL-encoded filters (auto-decoded by URLSearchParams)', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_pod_name' },
op: 'contains',
value: 'api-server',
},
],
op: 'AND',
};
const encodedValue = encodeURIComponent(JSON.stringify(filters));
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toEqual(filters);
});
it('should return null on malformed JSON instead of crashing', () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const searchParams = new URLSearchParams();
searchParams.set('filters', '{invalid-json}');
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toBeNull();
consoleSpy.mockRestore();
});
it('should handle filters with K8s container image names', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_container_name' },
op: '=',
value: 'registry.k8s.io/coredns/coredns:v1.10.1',
},
],
op: 'AND',
};
const encodedValue = encodeURIComponent(JSON.stringify(filters));
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toEqual(filters);
});
});
describe('regression: double decoding issue', () => {
it('should not crash when URL params are already decoded by URLSearchParams', () => {
// The key bug: URLSearchParams.get() auto-decodes, so encoding once in URL
// means .get() returns decoded value. Old code called decodeURIComponent()
// again which could crash on certain characters.
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_namespace_name' },
op: '=',
value: 'kube-system',
},
],
op: 'AND',
};
const encodedValue = encodeURIComponent(JSON.stringify(filters));
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
// This should work without crashing
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toEqual(filters);
});
it('should handle values with percent signs in labels', () => {
// K8s labels might contain literal "%" characters like "cpu-usage-50%"
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_label' },
op: '=',
value: 'cpu-50%',
},
],
op: 'AND',
};
const encodedValue = encodeURIComponent(JSON.stringify(filters));
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toEqual(filters);
});
it('should handle complex K8s deployment names with special chars', () => {
const filters = {
items: [
{
id: '1',
key: { key: 'k8s_deployment_name' },
op: '=',
value: 'nginx-ingress-controller',
},
],
op: 'AND',
};
const encodedValue = encodeURIComponent(JSON.stringify(filters));
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
const result = getFiltersFromParams(searchParams, 'filters');
expect(result).toEqual(filters);
});
});
});

View File

@@ -8,10 +8,13 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import PodDetails from 'container/InfraMonitoringK8s/Pods/PodDetails/PodDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('PodDetails', () => {
const mockPod = {
podName: 'test-pod',
@@ -25,13 +28,15 @@ describe('PodDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const clusterNameElements = screen.getAllByText('test-cluster');
@@ -49,13 +54,15 @@ describe('PodDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -73,13 +80,15 @@ describe('PodDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
@@ -88,13 +97,15 @@ describe('PodDetails', () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
@@ -105,13 +116,15 @@ describe('PodDetails', () => {
it('should close modal when close button is clicked', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>,
<Wrapper>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<MemoryRouter>
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });

View File

@@ -4,14 +4,16 @@ setupCommonMocks();
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 } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import StatefulSetDetails from 'container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails';
import store from 'store';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import { userEvent } from 'tests/test-utils';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('StatefulSetDetails', () => {
const mockStatefulSet = {
meta: {
@@ -23,8 +25,8 @@ describe('StatefulSetDetails', () => {
it('should render modal with relevant metadata', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
@@ -32,8 +34,8 @@ describe('StatefulSetDetails', () => {
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</Wrapper>,
);
const statefulSetNameElements = screen.getAllByText('test-stateful-set');
@@ -47,8 +49,8 @@ describe('StatefulSetDetails', () => {
it('should render modal with 4 tabs', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
@@ -56,8 +58,8 @@ describe('StatefulSetDetails', () => {
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByText('Metrics');
@@ -75,8 +77,8 @@ describe('StatefulSetDetails', () => {
it('default tab should be metrics', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
@@ -84,18 +86,18 @@ describe('StatefulSetDetails', () => {
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', () => {
it('should switch to events tab when events tab is clicked', async () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
@@ -103,20 +105,20 @@ describe('StatefulSetDetails', () => {
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
fireEvent.click(eventsTab);
await userEvent.click(eventsTab, { pointerEventsCheck: 0 });
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', () => {
it('should close modal when close button is clicked', async () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
@@ -124,12 +126,12 @@ describe('StatefulSetDetails', () => {
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(closeButton);
await userEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -4,14 +4,13 @@ setupCommonMocks();
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 { render, waitFor } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import K8sVolumesList from 'container/InfraMonitoringK8s/Volumes/K8sVolumesList';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { IAppContext, IUser } from 'providers/App/types';
import store from 'store';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
const queryClient = new QueryClient({
@@ -81,8 +80,8 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
it('should call aggregate keys API with k8s_volume_capacity', async () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
@@ -90,8 +89,8 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</NuqsTestingAdapter>,
);
await waitFor(() => {
@@ -130,8 +129,8 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
} as IAppContext);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
@@ -139,8 +138,8 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>,
</QueryClientProvider>
</NuqsTestingAdapter>,
);
await waitFor(() => {

View File

@@ -1,3 +1,4 @@
import { createElement } from 'react';
import * as appContextHooks from 'providers/App/App';
import * as timezoneHooks from 'providers/Timezone';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
@@ -45,14 +46,6 @@ const setupCommonMocks = (): void => {
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useSearchParams: jest.fn().mockReturnValue([
{
get: jest.fn(),
entries: jest.fn(() => []),
set: jest.fn(),
},
jest.fn(),
]),
useNavigationType: (): any => 'PUSH',
}));
@@ -103,6 +96,15 @@ const setupCommonMocks = (): void => {
safeNavigate: jest.fn(),
}),
}));
// TODO: Remove this when https://github.com/SigNoz/engineering-pod/issues/4253
jest.mock('container/TopNav/DateTimeSelectionV2', () => {
return {
__esModule: true,
default: (): React.ReactElement =>
createElement('div', { 'data-testid': 'datetime-selection' }),
};
});
};
export default setupCommonMocks;

View File

@@ -12,11 +12,7 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
getInvalidValueTooltipText,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from './constants';
import { getInvalidValueTooltipText, K8sCategory } from './constants';
/**
* Converts size in bytes to a human-readable string with appropriate units
@@ -254,27 +250,6 @@ export const filterDuplicateFilters = (
return uniqueFilters;
};
export const getOrderByFromParams = (
searchParams: URLSearchParams,
returnNullAsDefault = false,
): {
columnName: string;
order: 'asc' | 'desc';
} | null => {
const orderByFromParams = searchParams.get(
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
);
if (orderByFromParams) {
const decoded = decodeURIComponent(orderByFromParams);
const parsed = JSON.parse(decoded);
return parsed as { columnName: string; order: 'asc' | 'desc' };
}
if (returnNullAsDefault) {
return null;
}
return { columnName: 'cpu', order: 'desc' };
};
export const getFiltersFromParams = (
searchParams: URLSearchParams,
queryKey: string,
@@ -282,10 +257,9 @@ export const getFiltersFromParams = (
const filtersFromParams = searchParams.get(queryKey);
if (filtersFromParams) {
try {
const decoded = decodeURIComponent(filtersFromParams);
const parsed = JSON.parse(decoded);
const parsed = JSON.parse(filtersFromParams);
return parsed as IBuilderQuery['filters'];
} catch (error) {
} catch {
return null;
}
}

View File

@@ -0,0 +1,224 @@
import { VIEWS } from 'components/HostMetricsDetail/constants';
import {
Options,
parseAsInteger,
parseAsJson,
parseAsString,
useQueryState,
UseQueryStateReturn,
} from 'nuqs';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategories } from './constants';
import { orderBySchema, OrderBySchemaType } from './schemas';
const defaultFilters: IBuilderQuery['filters'] = { items: [], op: 'and' };
const defaultNuqsOptions: Options = {
history: 'push',
};
export const useInfraMonitoringCurrentPage = (): UseQueryStateReturn<
number,
number
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE,
parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringOrderBy = (): UseQueryStateReturn<
OrderBySchemaType,
OrderBySchemaType
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
parseAsJson(orderBySchema).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringOrderByHosts = (): UseQueryStateReturn<
OrderBySchemaType,
OrderBySchemaType
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
parseAsJson(orderBySchema)
.withDefault({
columnName: 'cpu',
order: 'desc',
})
.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringGroupBy = (): UseQueryStateReturn<
BaseAutocompleteData[],
[]
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY,
parseAsJsonNoValidate<IBuilderQuery['groupBy']>()
.withDefault([])
.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringView = (): UseQueryStateReturn<string, string> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW,
parseAsString.withDefault(VIEWS.METRICS).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringLogFilters = (): UseQueryStateReturn<
TagFilter,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS,
parseAsJsonNoValidate<IBuilderQuery['filters']>().withOptions(
defaultNuqsOptions,
),
);
export const useInfraMonitoringTracesFilters = (): UseQueryStateReturn<
TagFilter,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS,
parseAsJsonNoValidate<IBuilderQuery['filters']>().withOptions(
defaultNuqsOptions,
),
);
export const useInfraMonitoringEventsFilters = (): UseQueryStateReturn<
TagFilter,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS,
parseAsJsonNoValidate<IBuilderQuery['filters']>().withOptions(
defaultNuqsOptions,
),
);
export const useInfraMonitoringCategory = (): UseQueryStateReturn<
string,
string
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY,
parseAsString.withDefault(K8sCategories.PODS).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringFilters = (): UseQueryStateReturn<
string,
string
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
parseAsString.withDefault('').withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringFiltersK8s = (): UseQueryStateReturn<
TagFilter,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
parseAsJsonNoValidate<TagFilter>().withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringFiltersHosts = (): UseQueryStateReturn<
TagFilter,
TagFilter | undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
parseAsJsonNoValidate<IBuilderQuery['filters']>()
.withDefault(defaultFilters)
.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringClusterName = (): UseQueryStateReturn<
string,
string
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
parseAsString.withDefault('').withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringDaemonSetUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringDeploymentUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringJobUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringNamespaceUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringNodeUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringPodUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringStatefulSetUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
parseAsString.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringVolumeUID = (): UseQueryStateReturn<
string,
undefined
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
parseAsString.withOptions(defaultNuqsOptions),
);

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
export const orderBySchema = z
.object({
columnName: z.string(),
order: z.enum(['asc', 'desc']),
})
.nullable();
export type OrderBySchemaType = z.infer<typeof orderBySchema>;

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { TableColumnType as ColumnType } from 'antd';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import get from 'api/browser/localstorage/get';
import set from 'api/browser/localstorage/set';
import {
@@ -17,6 +16,7 @@ import {
ValidateColumnValueWrapper,
} from './commonUtils';
import { DEFAULT_PAGE_SIZE, K8sCategory } from './constants';
import { OrderBySchemaType } from './schemas';
import './InfraMonitoringK8s.styles.scss';
@@ -148,7 +148,7 @@ export const dummyColumnConfig = {
className: 'column column-dummy',
};
const columnsConfig = [
const columnsConfig: ColumnType<K8sPodsRowData>[] = [
{
title: <div className="column-header pod-name-header">Pod Name</div>,
dataIndex: 'podName',
@@ -231,7 +231,7 @@ const columnsConfig = [
// },
];
export const namespaceColumnConfig = {
export const namespaceColumnConfig: ColumnType<K8sPodsRowData> = {
title: <div className="column-header">Namespace</div>,
dataIndex: 'namespace',
key: 'namespace',
@@ -242,7 +242,7 @@ export const namespaceColumnConfig = {
className: 'column column-namespace',
};
export const nodeColumnConfig = {
export const nodeColumnConfig: ColumnType<K8sPodsRowData> = {
title: <div className="column-header">Node</div>,
dataIndex: 'node',
key: 'node',
@@ -253,7 +253,7 @@ export const nodeColumnConfig = {
className: 'column column-node',
};
export const clusterColumnConfig = {
export const clusterColumnConfig: ColumnType<K8sPodsRowData> = {
title: <div className="column-header">Cluster</div>,
dataIndex: 'cluster',
key: 'cluster',
@@ -264,7 +264,7 @@ export const clusterColumnConfig = {
className: 'column column-cluster',
};
export const columnConfigMap = {
export const columnConfigMap: Record<string, ColumnType<K8sPodsRowData>> = {
namespace: namespaceColumnConfig,
node: nodeColumnConfig,
cluster: clusterColumnConfig,
@@ -273,8 +273,9 @@ export const columnConfigMap = {
export const getK8sPodsListColumns = (
addedColumns: IEntityColumn[],
groupBy: IBuilderQuery['groupBy'],
defaultOrderBy: OrderBySchemaType,
): ColumnType<K8sPodsRowData>[] => {
const updatedColumnsConfig = [...columnsConfig];
const updatedColumnsConfig: ColumnType<K8sPodsRowData>[] = [...columnsConfig];
for (const column of addedColumns) {
const config = columnConfigMap[column.id as keyof typeof columnConfigMap];
@@ -293,7 +294,14 @@ export const getK8sPodsListColumns = (
return filteredColumns as ColumnType<K8sPodsRowData>[];
}
return updatedColumnsConfig as ColumnType<K8sPodsRowData>[];
for (const column of updatedColumnsConfig) {
if (column.sorter && column.key === defaultOrderBy?.columnName) {
column.defaultSortOrder =
defaultOrderBy?.order === 'asc' ? 'ascend' : 'descend';
}
}
return updatedColumnsConfig;
};
const dotToUnder: Record<string, keyof K8sPodsData['meta']> = {

View File

@@ -1,12 +1,13 @@
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import { isModifierKeyPressed } from 'utils/app';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -29,6 +30,7 @@ const alertLogEvents = (
export function AlertsEmptyState(): JSX.Element {
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const [addNewAlert] = useComponentPermission(
['add_new_alert', 'action'],
user.role,
@@ -36,10 +38,13 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback(() => {
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
}, []);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent) => {
setLoading(false);
safeNavigate(ROUTES.ALERTS_NEW, { newTab: isModifierKeyPressed(e) });
},
[safeNavigate],
);
return (
<div className="alert-list-container">

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { PlusOutlined } from '@ant-design/icons';
@@ -30,6 +30,7 @@ import { useAppContext } from 'providers/App/App';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
import { isModifierKeyPressed } from 'utils/app';
import DeleteAlert from './DeleteAlert';
import { ColumnButton, SearchContainer } from './styles';
@@ -99,16 +100,24 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
});
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(() => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent): void => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, {
newTab: isModifierKeyPressed(e),
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
[],
);
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
const onEditHandler = (
record: GettableAlert,
options?: { newTab?: boolean },
): void => {
const compositeQuery = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(record.condition.compositeQuery),
record.alertType as AlertTypes,
@@ -124,11 +133,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setEditLoader(false);
if (openInNewTab) {
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
} else {
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
newTab: options?.newTab,
});
};
const onCloneHandler = (
@@ -265,7 +272,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
e.stopPropagation();
e.preventDefault();
onEditHandler(record, e.metaKey || e.ctrlKey);
onEditHandler(record, { newTab: isModifierKeyPressed(e) });
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
@@ -330,7 +337,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
/>,
<ColumnButton
key="2"
onClick={(): void => onEditHandler(record, false)}
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { newTab: isModifierKeyPressed(e) })
}
type="link"
loading={editLoader}
>
@@ -338,7 +347,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</ColumnButton>,
<ColumnButton
key="3"
onClick={(): void => onEditHandler(record, true)}
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>

View File

@@ -82,6 +82,7 @@ import {
Widgets,
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
@@ -372,11 +373,7 @@ function DashboardsList(): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}
safeNavigate(getLink(), { newTab: isModifierKeyPressed(event) });
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,

View File

@@ -1,3 +1,16 @@
.live-logs-container {
display: flex;
flex: 1;
min-height: 0;
}
.live-logs-content {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.live-logs-chart-container {
height: 200px;
min-height: 200px;
@@ -5,6 +18,12 @@
border-right: none;
}
.live-logs-list-container {
display: flex;
flex: 1;
min-height: 0;
}
.live-logs-settings-panel {
display: flex;
align-items: center;

View File

@@ -18,6 +18,19 @@
}
.live-logs-list {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
.ant-card,
.ant-card-body {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
}
.live-logs-list-loading {
padding: 16px;
text-align: center;

View File

@@ -8,8 +8,8 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import TanStackTableView from 'container/LogsExplorerList/TanStackTableView';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
@@ -50,7 +50,7 @@ function LiveLogsList({
[logs],
);
const { options } = useOptionsMenu({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
aggregateOperator: StringOperators.NOOP,
@@ -158,7 +158,7 @@ function LiveLogsList({
{formattedLogs.length !== 0 && (
<InfinityWrapperStyled>
{options.format === OptionFormatTypes.TABLE ? (
<InfinityTableView
<TanStackTableView
ref={ref}
isLoading={false}
tableViewProps={{
@@ -174,6 +174,7 @@ function LiveLogsList({
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
onRemoveColumn={config.addColumn?.onRemove}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>

View File

@@ -4,18 +4,11 @@ import { FontSize } from 'container/OptionsMenu/types';
export const infinityDefaultStyles: CSSProperties = {
width: '100%',
overflowX: 'scroll',
marginTop: '15px',
};
export function getInfinityDefaultStyles(fontSize: FontSize): CSSProperties {
export function getInfinityDefaultStyles(_fontSize: FontSize): CSSProperties {
return {
width: '100%',
overflowX: 'scroll',
marginTop:
fontSize === FontSize.SMALL
? '10px'
: fontSize === FontSize.MEDIUM
? '12px'
: '15px',
};
}

View File

@@ -14,6 +14,78 @@ interface TableHeaderCellStyledProps {
export const TableStyled = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
`;
/**
* TanStack column sizing uses table-layout:fixed + colgroup widths; without clipping,
* cell content overflows visually on top of neighbouring columns (overlap / "ghost" text).
*/
export const TanStackTableStyled = styled(TableStyled)`
table-layout: fixed;
width: 100%;
min-width: 100%;
max-width: 100%;
& td,
& th {
overflow: hidden;
min-width: 0;
box-sizing: border-box;
vertical-align: middle;
}
& td.table-actions-cell {
overflow: visible;
}
& td.body {
word-break: break-word;
overflow-wrap: anywhere;
}
/* Let nested body HTML / line-clamp shrink inside fixed columns */
& td.body > * {
min-width: 0;
max-width: 100%;
}
/* Long column titles: ellipsis when wider than the column (TanStackHeaderRow) */
& thead th .tanstack-header-title {
min-width: 0;
flex: 1 1 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& thead th .tanstack-header-title > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
& td.logs-table-filler-cell,
& th.logs-table-filler-header {
padding: 0 !important;
min-width: 0;
border-left: none;
}
& th.logs-table-actions-header {
position: sticky;
right: 0;
z-index: 2;
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
padding: 0 !important;
overflow: visible;
white-space: nowrap;
border-left: none;
}
`;
const getTimestampColumnWidth = (
@@ -46,6 +118,19 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
${({ columnKey, $hasSingleColumn }): string =>
getTimestampColumnWidth(columnKey, $hasSingleColumn)}
&.table-actions-cell {
position: sticky;
right: 0;
z-index: 2;
width: 0;
min-width: 0;
max-width: 0;
padding: 0 !important;
white-space: nowrap;
overflow: visible;
background-color: inherit;
}
`;
export const TableRowStyled = styled.tr<{
@@ -64,7 +149,10 @@ export const TableRowStyled = styled.tr<{
position: relative;
.log-line-action-buttons {
display: none;
display: flex;
opacity: 0;
pointer-events: none;
transition: opacity 80ms linear;
}
&:hover {
@@ -73,13 +161,25 @@ export const TableRowStyled = styled.tr<{
getActiveLogBackground(true, $isDarkMode, $logType)}
}
.log-line-action-buttons {
display: flex;
opacity: 1;
pointer-events: auto;
}
}
${({ $isActiveLog }): string =>
$isActiveLog
? `
.log-line-action-buttons {
opacity: 1;
pointer-events: auto;
}
`
: ''}
`;
export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
padding: 0.5rem;
height: 36px;
text-align: left;
font-size: 14px;
font-style: normal;
font-weight: 400;
@@ -98,6 +198,12 @@ export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
: ``};
${({ $isLogIndicator }): string =>
$isLogIndicator ? 'padding: 0px; width: 1%;' : ''}
border-top: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l2-border);
box-shadow: inset 0 -1px 0 var(--l2-border);
&:first-child {
border-left: 1px solid var(--l2-border);
}
color: ${(props): string =>
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};

View File

@@ -5,6 +5,8 @@ import { ILog } from 'types/api/logs/log';
export type InfinityTableProps = {
isLoading?: boolean;
isFetching?: boolean;
onRemoveColumn?: (columnKey: string) => void;
tableViewProps: Omit<UseTableViewProps, 'onOpenLogsContext' | 'onClickExpand'>;
infitiyTableProps?: {
onEndReached: (index: number) => void;

View File

@@ -8,7 +8,7 @@
line-height: 18px;
letter-spacing: -0.005em;
text-align: left;
min-height: 500px;
min-height: 0;
.logs-list-table-view-container {
.data-table-container {
@@ -24,11 +24,11 @@
color: white !important;
.cursor-col-resize {
width: 3px !important;
width: 24px !important;
cursor: col-resize !important;
opacity: 0.5 !important;
background-color: var(--bg-ink-500) !important;
border: 1px solid var(--bg-ink-500) !important;
opacity: 1 !important;
background-color: transparent !important;
border: none !important;
&:hover {
opacity: 1 !important;
@@ -85,7 +85,7 @@
}
thead {
z-index: 0 !important;
z-index: 2 !important;
}
.log-state-indicator {

View File

@@ -0,0 +1,56 @@
import { ComponentProps } from 'react';
import { TableComponents } from 'react-virtuoso';
import {
getLogIndicatorType,
getLogIndicatorTypeForTable,
} from 'components/Logs/LogStateIndicator/utils';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ILog } from 'types/api/logs/log';
import { TableRowStyled } from '../InfinityTableView/styles';
import { TanStackTableRowData } from './types';
type VirtuosoTableRowProps = ComponentProps<
NonNullable<TableComponents<TanStackTableRowData>['TableRow']>
>;
export type TanStackCustomTableRowProps = VirtuosoTableRowProps & {
activeLog?: ILog | null;
activeContextLog?: ILog | null;
logsById: Map<string, ILog>;
};
function TanStackCustomTableRow({
children,
item,
activeLog,
activeContextLog,
logsById,
...props
}: TanStackCustomTableRowProps): JSX.Element {
const { isHighlighted } = useCopyLogLink(item.currentLog.id);
const isDarkMode = useIsDarkMode();
const rowId = String(item.currentLog.id ?? '');
const rowLog = logsById.get(rowId) || item.currentLog;
const logType = rowLog
? getLogIndicatorType(rowLog)
: getLogIndicatorTypeForTable(item.log);
return (
<TableRowStyled
{...props}
$isDarkMode={isDarkMode}
$isActiveLog={
isHighlighted ||
rowId === String(activeLog?.id ?? '') ||
rowId === String(activeContextLog?.id ?? '')
}
$logType={logType}
>
{children}
</TableRowStyled>
);
}
export default TanStackCustomTableRow;

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