Compare commits

...

47 Commits

Author SHA1 Message Date
grandwizard28
77493f7c76 chore: address review comments
Comment out SourceAudit from Enum() until frontend is ready.
Use actual audit table constants in metadata test helpers.
2026-04-02 22:52:28 +05:30
grandwizard28
b1f7e5a3df feat(audit): add telemetry audit query infrastructure
Add pkg/telemetryaudit/ with tables, field mapper, condition builder,
and statement builder for querying audit logs from signoz_audit database.
Add SourceAudit to source enum and integrate audit key resolution
into the metadata store.
2026-04-02 16:41:58 +05:30
SagarRajput-7
b6b689902d feat: added doc links for service account and misc changes (#10804)
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
* feat: added doc links for service account and misc changes

* feat: remove announcement and presisted announcement banner, since we app this in periscope library

* feat: updated banner text
2026-04-02 08:22:31 +00:00
Naman Verma
65402ca367 fix: warning instead of error for dormant metrics in query range API (#10737)
* fix: warning instead of error for dormant metrics in query range API

* fix: add missing else

* fix: keep track of present aggregations

* fix: note present aggregation after type is set

* test: integration test fix and new test

* chore: lint errors

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-04-02 07:25:29 +00:00
Tushar Vats
f71d5bf8f1 fix: added validations for having expression (#10286)
* fix: added validations for having expression

* fix: added extra validation and unit tests

* fix: added antlr based parsing for validation

* fix: added more unit tests

* fix: removed validation on having in range request validations

* fix: generated lexer files and added more unit tests

* fix: edge cases

* fix: added cmnd to scripts for generating lexer

* fix: use std libg sorting instead of selection sort

* fix: support implicit and

* fix: allow bare not in expression

* fix: added suggestion for having expression

* fix: typo

* fix: added more unit tests, handle white space difference in aggregation exp and having exp

* fix: added support for in and not, updated errors

* fix: added support for brackets list

* fix: lint error

* fix: handle non spaced expression

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-04-02 03:52:11 +00:00
Srikanth Chekuri
5abfd0732a chore: remove deprecated v3/v4 support in rules (#10760)
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
* chore: remove deprecated v3/v4 support in rules

* chore: fix test

* chore: fix logs

* chore: fix logging

* chore: fix ci

* chore: address review comments
2026-04-01 19:48:37 +00:00
swapnil-signoz
4f273b296e refactor: removing dashboard overview images (#10801)
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
2026-04-01 16:25:55 +00:00
swapnil-signoz
3dc0a7c8ce feat: adding aws service definitions in types (#10798)
* feat: adding aws service definitions in types

* refactor: moving definitions to module fs
2026-04-01 14:08:46 +00:00
Liapis Nikolaos
1080553905 feat(logs): pretty-print JSON attribute values when copying to clipboard (#10778)
When copying log attribute values that contain valid JSON objects or
arrays, the value is now pretty-printed with 2-space indentation.
This makes it easy to paste into JSON tools or editors.

Non-JSON values (strings, numbers, booleans) are unaffected.

Closes #8208
2026-04-01 13:01:09 +00:00
Nikhil Soni
23e3c75d24 feat: return all spans for flamegraph under a limit (#10757)
* feat: return all spans for flamegraph under a limit

* feat: increase fg limits and add timestamp boundaries

* fix: set default value for ts boundary

* fix: use correct value for boundary end ts

* chore: change info log of flamegraph to debug
2026-04-01 10:18:36 +00:00
Pandey
42415e0873 feat(audit): handler-level AuditDef, audit middleware, and response capture (#10791)
* feat(audit): handler-level AuditDef and response-capturing wrapper

Add declarative audit instrumentation to the handler package. Routes
declare an AuditDef alongside OpenAPIDef; the handler automatically
captures the response status/body and emits an audit event via
auditor.Audit() after every request.

* refactor(audit): move audit logic to middleware, merge with logging

Move audit event emission from handler to middleware layer. The handler
package keeps only the AuditDef struct and AuditDefProvider interface.
The logging middleware now handles both request logging and audit event
emission using a single response capture, avoiding double-wrapping.

Rename badResponseLoggingWriter to responseCapture with body capture
on all 4xx/5xx responses (previously only 400 and 5xx).

* refactor(audit): rename Logging middleware to Audit, merge into single file

Delete logging.go and merge its contents into audit.go. Rename
Logging/NewLogging to Audit/NewAudit. The response.go file with
responseCapture is unchanged.

* refactor(audit): extract NewAuditEventFromHTTPRequest factory into audittypes

Move event construction to audittypes.NewAuditEventFromHTTPRequest with
an AuditEventContext struct for caller-provided fields. The audittypes
layer reads only transport fields from *http.Request and has no mux,
authtypes, or context dependencies. The middleware pre-extracts
principal, trace, error, and route fields before calling the factory.

* refactor(audit): move error parsing to render.ErrorFromBody and render.ErrorTypeFromStatusCode

Add render.ErrorFromBody to extract errors.JSON from a JSON-encoded
ErrorResponse body, and render.ErrorTypeFromStatusCode to reverse-map
HTTP status codes to error type strings. The middleware now uses these
instead of local duplicates.

* refactor(audit): move AuditDef onto Handler interface, consolidate files

Move AuditDef() onto the Handler interface directly. All Handler
implementations now carry it: handler returns the configured def,
healthOpenAPIHandler returns nil. Delete the separate AuditDefProvider
interface and audit.go handler file. Move excludedRoutes check before
audit emission so excluded routes skip both logging and audit.

* feat(audit): add option.go with AuditDef, Option, and WithAuditDef

* refactor(audit): decompose AuditEvent into attribute sub-structs, add tests

Decompose flat AuditEvent fields into typed sub-structs
(AuditEventAuditAttributes, PrincipalAttributes, ResourceAttributes,
ErrorAttributes, TransportAttributes) each with a constructor and
Put(pcommon.Map) method. Simplify NewAuditEventFromHTTPRequest to
accept authtypes.Claims and oteltrace IDs directly. Simplify the
middleware caller accordingly.

Add unit tests for the factory, outcome boundary, and principal type
derivation.

* refactor(audit): shorten attribute struct names, drop error message

Rename AuditEventAuditAttributes to AuditAttributes,
AuditEventPrincipalAttributes to PrincipalAttributes, and likewise
for Resource, Error, and Transport. The package prefix already
disambiguates.

Remove ErrorMessage from ErrorAttributes to avoid leaking sensitive
or PII data into audit logs. Error type and code are sufficient for
filtering; investigators can correlate via trace ID.

* fix(audit): update auditorserver test and otlphttp provider for new struct layout

Update newTestEvent in server_test.go to use nested AuditAttributes
and ResourceAttributes. Update otlphttpauditor provider to access
PrincipalOrgID via PrincipalAttributes. Fix godot lint on attribute
section comments.

* fix(audit): fix gjson path in ErrorCodeFromBody, add tests

Fix ErrorCodeFromBody gjson path from "errors.code" to "error.code"
to match the ErrorResponse JSON structure. Add unit tests for valid
error response and invalid JSON cases.

* fix(audit): add CodeUnset, use ErrorCodeFromBody in middleware

Add errors.CodeUnset for responses missing an error code. Update the
audit middleware to use render.ErrorCodeFromBody instead of the removed
render.ErrorFromBody.

* test(audit): add unit tests for responseCapture

Test the four meaningful behaviors: success responses don't capture
body, error responses capture body, large error bodies truncate at
4096 bytes, and 204 No Content suppresses writes entirely.

* fix(audit): check rw.Write return values in response_test.go

* style(audit): rename want prefix to expected in test fields

* refactor(audit): replace Sprintf with strings.Builder in newBody

Handle edge cases where principal email, ID, or resource ID may be
empty. The builder conditionally includes each segment, avoiding
empty parentheses or leading spaces in the audit body.

Add test cases covering all meaningful combinations: success/failure
with full/partial/empty principal, resource ID, and error details.

* chore: fix formatting

* chore: remove json tags

* fix: rebase with main
2026-04-01 10:10:52 +00:00
Vikrant Gupta
bad80399a6 feat(serviceaccount): integrate service account (#10681)
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
* feat(serviceaccount): integrate service account

* feat(serviceaccount): integrate service account with better types

* feat(serviceaccount): fix lint and testing changes

* feat(serviceaccount): update integration tests

* feat(serviceaccount): fix formatting

* feat(serviceaccount): fix openapi spec

* feat(serviceaccount): update txlock to immediate to avoid busy snapshot errors

* feat(serviceaccount): add restrictions for factor_api_key

* feat(serviceaccount): add restrictions for factor_api_key

* feat: enabled service account and deprecated API Keys (#10715)

* feat: enabled service account and deprecated API Keys

* feat: deprecated API Keys

* feat: service account spec updates and role management changes

* feat: updated the error component for roles management

* feat: updated test case

* feat: updated the error component and added retries

* feat: refactored code and added retry to happend 3 times total

* feat: fixed feedbacks and added test case

* feat: refactored code and removed retry

* feat: updated the test cases

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
2026-04-01 07:20:59 +00:00
Vinicius Lourenço
e2cd203c8f test(k8s-volume-list): mark test as skip due to flakyness (#10787)
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
2026-03-31 16:57:38 +00:00
Vikrant Gupta
a4c6394542 feat(sqlstore): add support for transaction modes (#10781)
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
2026-03-31 13:40:06 +00:00
Pandey
71a13b4818 feat(audit): enterprise auditor with licensing gate and OTLP HTTP export (#10776)
* feat(audit): add enterprise auditor with licensing gate and OTLP HTTP export

Implements the enterprise auditor at ee/auditor/otlphttpauditor/.
Composes auditorserver.Server for batching with licensing gate,
OTel SDK LoggerProvider for InstrumentationScope, and otlploghttp
exporter for OTLP HTTP transport.

* fix(audit): address PR review — inline factory, move body+logrecord to audittypes

- Inline NewFactory closure, remove newProviderFunc
- Move buildBody to audittypes.BuildBody
- Move eventToLogRecord to audittypes.ToLogRecord
- go mod tidy for newly direct otel/log deps

* fix(audit): address PR review round 2

- Make ToLogRecord a method on AuditEvent returning sdklog.Record
- Fold buildBody into ToLogRecord as unexported helper
- Remove accumulatingProcessor and LoggerProvider — export directly
  via otlploghttp.Exporter
- Delete body.go and processor.go

* fix(audit): address PR review round 3

- Merge export.go into provider.go
- Add severity/severityText fields to Outcome struct
- Rename buildBody to setBody method on AuditEvent
- Add appendStringIfNotEmpty helper to reduce duplication

* feat(audit): switch to plog + direct HTTP POST for OTLP export

Replace otlploghttp.Exporter + sdklog.Record with plog data model,
ProtoMarshaler, and direct HTTP POST. This properly sets
InstrumentationScope.Name = "signoz.audit" and Resource attributes
on the OTLP payload.

* fix(audit): adopt collector otlphttpexporter pattern for HTTP export

Model the send function after the OTel Collector's otlphttpexporter:
- Bounded response body reads (64KB max)
- Protobuf-encoded Status parsing from error responses
- Proper response body draining on defer
- Detailed error messages with endpoint URL and status details

* refactor(audit): split export logic into export.go, add throttle retry

- Move export, send, and HTTP response helpers to export.go
- Add exporterhelper.NewThrottleRetry for 429/503 with Retry-After
- Parse Retry-After as delay-seconds or HTTP-date per RFC 7231
- Keep provider.go focused on Auditor interface and lifecycle

* feat(audit): add partial success handler and internal retry with backoff

- Parse ExportLogsServiceResponse on 2xx for partial success, log
  warning if log records were rejected
- Internal retry loop with exponential backoff for retryable status
  codes (429, 502, 503, 504) using RetryConfig from auditor config
- Honour Retry-After header (delay-seconds and HTTP-date)
- Store full auditor.Config on provider struct
- Replace exporterhelper.NewThrottleRetry with local retryableError
  type consumed by the internal retry loop

* fix(audit): fix lint — use pkg/errors, remove stdlib errors and fmt.Errorf

* refactor(audit): use provider as receiver name instead of p

* refactor(audit): clean up enterprise auditor implementation

- Extract retry logic into retry.go
- Move NewPLogsFromAuditEvents and ToLogRecord into event.go
- Add ErrCodeAuditExportFailed to auditor package
- Add version.Build to provider for service.version attribute
- Simplify sendOnce, split response handling into onSuccess/onErr
- Use PrincipalOrgID as valuer.UUID directly
- Use OTLPHTTP.Endpoint as URL type
- Remove gzip compression handling
- Delete logrecord.go

* refactor(audit): use pkg/http/client instead of bare http.Client

Use the standard SigNoz HTTP client with OTel instrumentation.
Disable heimdall retries (count=0) since we have our own
OTLP-aware retry loop that understands Retry-After headers.

* refactor(audit): use heimdall Retriable for retry instead of manual loop

- Implement retrier with exponential backoff from auditor RetryConfig
- Compute retry count from MaxElapsedTime and backoff intervals
- Pass retrier and retry count to pkg/http/client via WithRetriable
- Remove manual retry loop, retryableError type, and Retry-After parsing
- Heimdall handles retries on >= 500 status codes automatically

* refactor(audit): rename retry.go to retrier.go
2026-03-31 13:37:20 +00:00
Vinicius Lourenço
a8e2155bb6 fix(app-routes-redirect): redirects when workspaceBlocked & onboarding having wrong redirects (#10738)
* fix(private): issues with redirect when onboarding & workspace locked

* test(app-routes): add tests for private route
2026-03-31 11:07:42 +00:00
Vinicius Lourenço
a9cbf9a4df fix(infra-monitoring): request loop when click to visualize volume (#10657)
* fix(infra-monitoring): request loop when click to visualize volume

* test(k8s-volume-list): fix tests broken due to nuqs
2026-03-31 10:56:09 +00:00
Vikrant Gupta
2163e1ce41 chore(lint): enable godot and staticcheck (#10775)
* chore(lint): enable godot and staticcheck

* chore(lint): merge main and fix new lint issues in main
2026-03-31 09:11:49 +00:00
Srikanth Chekuri
b9eecacab7 chore: remove v1 metrics explorer code (#10764)
* chore: remove v1 metrics explorer code

* chore: fix ci

* chore: fix tests

* chore: address review comments
2026-03-31 08:46:27 +00:00
Tushar Vats
13982033dc fix: handle empty not() expression (#10165)
* fix: handle empty not() expression

* fix: handle more cases

* fix: short circuit conditions and updated unit tests

* fix: revert commented code

* fix: added more unit tests

* fix: added integration tests

* fix: make py-lint and make py-fmt

* fix: moved from traces to logs for testing full text search

* fix: simplify code

* fix: added more unit tests

* fix: addressed comments

* fix: update comment

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

* fix: update unit test

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

* fix: update unit test

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

* fix: instead of using true, using a skip literal

* fix: unit test

* fix: update integration test

* fix: update unit for relevance

* fix: lint error

* fix: added a new literal for error condition, added more unit tests

* fix: merge issues

* fix: inline comments

* fix: update unit tests merging from main

* fix: make py-fmt and make py-lint

* fix: type handling

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-03-31 08:44:07 +00:00
Piyush Singariya
b198cfc11a chore: enable JSON Path index in JSON Logs (#10736)
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
* feat: enable JSON Path index

* fix: contextual path index usage

* test: fix unit tests
2026-03-31 08:07:27 +00:00
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
Nityananda Gohain
e4f0d026c1 feat: time aware dynamic field mapper (#9669)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
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
* feat: time aware dynamic field mapper

* fix: tests

* fix: update fetch code

* fix: update comment

* fix: remove goroutine

* fix: update tests

* fix: minor changes

* fix: minor changes

* fix: use proper cache

* fix: use orgId properly

* fix: aggregation

* fix: comments

* fix: test

* fix: lint issues

* fix: minor changes

* fix: address changes and add tests

* fix: use name from evolutions

* fix: make the evolution code reusable and propagte context properly

* fix: tests

* fix: update the evolution metadata table

* fix: lint issues

* fix: address cursor comments

* fix: tests

* fix: tests

* fix: changes

* fix: refactor code

* fix: more changes

* fix: clean up evolution logic

* fix: fetch keys at the start

* fix: more changes

* fix: structural changes

* fix: add api

* fix: minor cleanup

* fix: update query in adjust keys

* fix: edgecases

* fix: update conditionfor

* fix: address comments

* fix: revert commented test

* fix: minor refactoring and addressing comments

* fix: evolution metadata

* fix: more fixes

* fix: add support for version in the evolutions

* fix: final cleanup

* fix: copy slice

* fix: address comments

* fix: address comments

* fix: field_mapper

* chore: add integration tests

* fix: lint

* fix: lint

* chore: integration test for materialized

* chore: add remaining changes

* chore: fix lint in integration tests

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-03-28 05:12:49 +00:00
Debopam Roy
754dbc7f38 Feat/external api dependent service different color (#9412)
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
* feat: change the cursor to pointer on hovering domain rows

* feat: all the columns have same background

* feat: external api -> domains table -> right drawer -> all the columns have same color

* fix(domain-details): keep background of progress-bar

---------

Co-authored-by: Vinícius Lourenço <vinicius@signoz.io>
2026-03-27 18:20:25 +00:00
xi7ang
07dbf1e69f fix: match light mode border color in External APIs page (#10544)
* fix(ui): API Monitoring light mode border color consistency (#9402)

* fix(explorer): little issues on css

* fix(explorer): not formatted correctly

---------

Co-authored-by: bwang <bwang@openclaw.ai>
Co-authored-by: Vinícius Lourenço <vinicius@signoz.io>
2026-03-27 17:21:41 +00:00
Vinicius Lourenço
abe5454d11 fix(member-drawer): use hook to copy to support more browsers (#10729)
* fix(member-drawer): use hook to copy to support more browsers

* chore(eslintrc): typo on error message for new lint rule
2026-03-27 14:44:16 +00:00
Vinicius Lourenço
749884ce70 fix(alert-rules-history): crash due to relativeTime empty (#10733) 2026-03-27 14:40:50 +00:00
Vinicius Lourenço
70fdc88112 fix(alerts): alert header breaking with unknown severity (#10730)
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
2026-03-27 13:52:17 +00:00
Ashwin Bhatkal
234787f2c1 fix: fallback to raw param if decodeURIComponent fails for dashboard variables (#10695)
* fix: fallback to raw param if decodeURIComponent fails for dashboard variables

* test: add tests for decodeURIComponent fallback in useVariablesFromUrl
2026-03-27 13:41:34 +00:00
Ashwin Bhatkal
e7d0dd8850 fix: guard getErrorDetails call against non-APIError instances in GridCard (#10700)
* fix: guard getErrorDetails call against non-APIError instances in GridCard

* test: add tests for errorDetails with APIError and plain Error
2026-03-27 13:40:05 +00:00
Ashwin Bhatkal
c23c6c8c27 fix: update panel waiting state condition (#10702)
* fix: update variable waiting state condition

* fix: remove dynamic variable waiting
2026-03-27 13:39:32 +00:00
Nikhil Soni
58dabf9a6c feat(waterfall): return all span for small traces and nested level for large (#10399)
* feat: add support for configurable pre expanded levels

When a span is expanded, now it will return nested children
up to a max depth level

* feat: show spinner on expanding span and fix autoscroll

Two improvemnts are included in this commit:
- Show a loading spiner in UI when a span is expanded
  to signal api call.
- Don't auto scroll the waterfall to bring the selected
  span to centre in case it's expanded or scrolled up
  because it looks very flickering behaviour to user

* feat: add support for returning all the spans

If total spans are less than a max value.
This allows UI to not have to call API
for smaller spans on ever collapse/uncollapse

* Revert "feat: show spinner on expanding span and fix autoscroll"

This reverts commit 6bd194dfb4.

* chore: add tests

* chore: make the code changes more easy to understand

* chore: cleanup traversal in trace waterfall

* chore: rename variables to be self descriptive

* chore: add rests for auto expand

* chore: add tests for getAllSpans

---------

Co-authored-by: nityanandagohain <nityanandagohain@gmail.com>
2026-03-27 12:13:53 +00:00
Vinicius Lourenço
91edc2a895 fix(infra-monitoring): not fetching correct group by keys (#10651) 2026-03-27 11:42:25 +00:00
SagarRajput-7
2e70857554 fix: remove custom domain from self hosted deployments (#10731)
* feat: remove custom domain from self hosted deployments

* fix: updated errors to surface from BE in invite member modal

* fix: updated test cases
2026-03-27 09:24:20 +00:00
Nityananda Gohain
f3e6892d5b fix: remove flakyness for trace waterfall tests (#10734) 2026-03-27 06:50:42 +00:00
Nityananda Gohain
23a4960e74 chore: don't run functions if the series is empty (#10725)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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
* chore: funcRunningDiff don't panic when series is empty

* chore: don't run functions if the series is empty
2026-03-27 04:41:00 +00:00
Vinicius Lourenço
5d0c55d682 fix(alerts-history): formatTime expecting number but receiving string (#10719)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-03-27 00:52:40 +00:00
728 changed files with 107904 additions and 18628 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

@@ -51,6 +51,7 @@ jobs:
- alerts
- ingestionkeys
- rootuser
- serviceaccount
sqlstore-provider:
- postgres
- sqlite

View File

@@ -6,12 +6,14 @@ linters:
- depguard
- errcheck
- forbidigo
- godot
- govet
- iface
- ineffassign
- misspell
- nilnil
- sloglint
- staticcheck
- wastedassign
- unparam
- unused

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

@@ -85,10 +85,12 @@ sqlstore:
sqlite:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
# Mode is the mode to use for the sqlite database.
# The journal mode for the sqlite database. Supported values: delete, wal.
mode: delete
# BusyTimeout is the timeout for the sqlite database to wait for a lock.
# The timeout for the sqlite database to wait for a lock.
busy_timeout: 10s
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
transaction_mode: deferred
##################### APIServer #####################
apiserver:
@@ -352,3 +354,13 @@ identn:
impersonation:
# toggle impersonation identN, when enabled, all requests will impersonate the root user
enabled: false
##################### Service Account #####################
serviceaccount:
email:
# email domain for the service account principal
domain: signozserviceaccount.com
analytics:
# toggle service account analytics
enabled: true

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ func (hp *HourlyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider {
return &hp.BaseSeasonalProvider
}
// NewHourlyProvider now uses the generic option type
// NewHourlyProvider now uses the generic option type.
func NewHourlyProvider(opts ...GenericProviderOption[*HourlyProvider]) *HourlyProvider {
hp := &HourlyProvider{
BaseSeasonalProvider: BaseSeasonalProvider{},

View File

@@ -32,7 +32,7 @@ func (s Seasonality) IsValid() bool {
}
type AnomaliesRequest struct {
Params qbtypes.QueryRangeRequest
Params *qbtypes.QueryRangeRequest
Seasonality Seasonality
}
@@ -47,7 +47,7 @@ type AnomaliesResponse struct {
// | |
// (rounded value for past peiod) + (seasonal growth)
//
// score = abs(value - prediction) / stddev (current_season_query)
// score = abs(value - prediction) / stddev (current_season_query).
type anomalyQueryParams struct {
// CurrentPeriodQuery is the query range params for period user is looking at or eval window
// Example: (now-5m, now), (now-30m, now), (now-1h, now)
@@ -81,7 +81,7 @@ type anomalyQueryParams struct {
Past3SeasonQuery qbtypes.QueryRangeRequest
}
func prepareAnomalyQueryParams(req qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
func prepareAnomalyQueryParams(req *qbtypes.QueryRangeRequest, seasonality Seasonality) *anomalyQueryParams {
start := req.Start
end := req.End

View File

@@ -18,12 +18,12 @@ var (
movingAvgWindowSize = 7
)
// BaseProvider is an interface that includes common methods for all provider types
// BaseProvider is an interface that includes common methods for all provider types.
type BaseProvider interface {
GetBaseSeasonalProvider() *BaseSeasonalProvider
}
// GenericProviderOption is a generic type for provider options
// GenericProviderOption is a generic type for provider options.
type GenericProviderOption[T BaseProvider] func(T)
func WithQuerier[T BaseProvider](querier querier.Querier) GenericProviderOption[T] {
@@ -121,7 +121,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
}
// getMatchingSeries gets the matching series from the query result
// for the given series
// for the given series.
func (p *BaseSeasonalProvider) getMatchingSeries(_ context.Context, queryResult *qbtypes.TimeSeriesData, series *qbtypes.TimeSeries) *qbtypes.TimeSeries {
if queryResult == nil || len(queryResult.Aggregations) == 0 || len(queryResult.Aggregations[0].Series) == 0 {
return nil
@@ -155,13 +155,14 @@ func (p *BaseSeasonalProvider) getStdDev(series *qbtypes.TimeSeries) float64 {
avg := p.getAvg(series)
var sum float64
for _, smpl := range series.Values {
sum += math.Pow(smpl.Value-avg, 2)
d := smpl.Value - avg
sum += d * d
}
return math.Sqrt(sum / float64(len(series.Values)))
}
// getMovingAvg gets the moving average for the given series
// for the given window size and start index
// for the given window size and start index.
func (p *BaseSeasonalProvider) getMovingAvg(series *qbtypes.TimeSeries, movingAvgWindowSize, startIdx int) float64 {
if series == nil || len(series.Values) == 0 {
return 0
@@ -236,7 +237,7 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
// getBounds gets the upper and lower bounds for the given series
// for the given z score threshold
// moving avg of the previous period series + z score threshold * std dev of the series
// moving avg of the previous period series - z score threshold * std dev of the series
// moving avg of the previous period series - z score threshold * std dev of the series.
func (p *BaseSeasonalProvider) getBounds(
series, predictedSeries, weekSeries *qbtypes.TimeSeries,
zScoreThreshold float64,
@@ -269,7 +270,7 @@ func (p *BaseSeasonalProvider) getBounds(
// getExpectedValue gets the expected value for the given series
// for the given index
// prevSeriesAvg + currentSeasonSeriesAvg - mean of past season series, past2 season series and past3 season series
// prevSeriesAvg + currentSeasonSeriesAvg - mean of past season series, past2 season series and past3 season series.
func (p *BaseSeasonalProvider) getExpectedValue(
_, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries, idx int,
) float64 {
@@ -283,7 +284,7 @@ func (p *BaseSeasonalProvider) getExpectedValue(
// getScore gets the anomaly score for the given series
// for the given index
// (value - expectedValue) / std dev of the series
// (value - expectedValue) / std dev of the series.
func (p *BaseSeasonalProvider) getScore(
series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries, value float64, idx int,
) float64 {
@@ -296,7 +297,7 @@ func (p *BaseSeasonalProvider) getScore(
// getAnomalyScores gets the anomaly scores for the given series
// for the given index
// (value - expectedValue) / std dev of the series
// (value - expectedValue) / std dev of the series.
func (p *BaseSeasonalProvider) getAnomalyScores(
series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries,
) *qbtypes.TimeSeries {

View File

@@ -0,0 +1,143 @@
package otlphttpauditor
import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/audittypes"
collogspb "go.opentelemetry.io/proto/otlp/collector/logs/v1"
"google.golang.org/protobuf/proto"
spb "google.golang.org/genproto/googleapis/rpc/status"
)
const (
maxHTTPResponseReadBytes int64 = 64 * 1024
protobufContentType string = "application/x-protobuf"
)
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
request, err := provider.marshaler.MarshalLogs(logs)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to marshal audit logs")
}
if err := provider.send(ctx, request); err != nil {
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
return err
}
return nil
}
// Posts a protobuf-encoded OTLP request to the configured endpoint.
// Retries are handled by the underlying heimdall HTTP client.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/otlphttpexporter/otlp.go
func (provider *provider) send(ctx context.Context, body []byte) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, provider.config.OTLPHTTP.Endpoint.String(), bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", protobufContentType)
res, err := provider.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.CopyN(io.Discard, res.Body, maxHTTPResponseReadBytes)
res.Body.Close()
}()
if res.StatusCode >= 200 && res.StatusCode <= 299 {
provider.onSuccess(ctx, res)
return nil
}
return provider.onErr(res)
}
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L403.
func (provider *provider) onSuccess(ctx context.Context, res *http.Response) {
resBytes, err := readResponseBody(res)
if err != nil || resBytes == nil {
return
}
exportResponse := &collogspb.ExportLogsServiceResponse{}
if err := proto.Unmarshal(resBytes, exportResponse); err != nil {
return
}
ps := exportResponse.GetPartialSuccess()
if ps == nil {
return
}
if ps.GetErrorMessage() != "" || ps.GetRejectedLogRecords() != 0 {
provider.settings.Logger().WarnContext(ctx, "partial success response", slog.String("message", ps.GetErrorMessage()), slog.Int64("dropped_log_records", ps.GetRejectedLogRecords()))
}
}
func (provider *provider) onErr(res *http.Response) error {
status := readResponseStatus(res)
if status != nil {
return errors.Newf(errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "request to %s responded with status code %d, Message=%s, Details=%v", provider.config.OTLPHTTP.Endpoint.String(), res.StatusCode, status.Message, status.Details)
}
return errors.Newf(errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "request to %s responded with status code %d", provider.config.OTLPHTTP.Endpoint.String(), res.StatusCode)
}
// Reads at most maxHTTPResponseReadBytes from the response body.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L275.
func readResponseBody(resp *http.Response) ([]byte, error) {
if resp.ContentLength == 0 {
return nil, nil
}
maxRead := resp.ContentLength
if maxRead == -1 || maxRead > maxHTTPResponseReadBytes {
maxRead = maxHTTPResponseReadBytes
}
protoBytes := make([]byte, maxRead)
n, err := io.ReadFull(resp.Body, protoBytes)
if n == 0 && (err == nil || errors.Is(err, io.EOF)) {
return nil, nil
}
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return nil, err
}
return protoBytes[:n], nil
}
// Decodes a protobuf-encoded Status from 4xx/5xx response bodies. Returns nil if the response is empty or cannot be decoded.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L310.
func readResponseStatus(resp *http.Response) *spb.Status {
if resp.StatusCode < 400 || resp.StatusCode > 599 {
return nil
}
respBytes, err := readResponseBody(resp)
if err != nil || respBytes == nil {
return nil
}
respStatus := &spb.Status{}
if err := proto.Unmarshal(respBytes, respStatus); err != nil {
return nil
}
return respStatus
}

View File

@@ -0,0 +1,97 @@
package otlphttpauditor
import (
"context"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
"github.com/SigNoz/signoz/pkg/factory"
client "github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/version"
"go.opentelemetry.io/collector/pdata/plog"
)
var _ auditor.Auditor = (*provider)(nil)
type provider struct {
settings factory.ScopedProviderSettings
config auditor.Config
licensing licensing.Licensing
build version.Build
server *auditorserver.Server
marshaler plog.ProtoMarshaler
httpClient *client.Client
}
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("otlphttp"), func(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
return newProvider(ctx, providerSettings, config, licensing, build)
})
}
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config auditor.Config, licensing licensing.Licensing, build version.Build) (auditor.Auditor, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/auditor/otlphttpauditor")
httpClient, err := client.New(
settings.Logger(),
providerSettings.TracerProvider,
providerSettings.MeterProvider,
client.WithTimeout(config.OTLPHTTP.Timeout),
client.WithRetryCount(retryCountFromConfig(config.OTLPHTTP.Retry)),
retrierOption(config.OTLPHTTP.Retry),
)
if err != nil {
return nil, err
}
provider := &provider{
settings: settings,
config: config,
licensing: licensing,
build: build,
marshaler: plog.ProtoMarshaler{},
httpClient: httpClient,
}
server, err := auditorserver.New(settings,
auditorserver.Config{
BufferSize: config.BufferSize,
BatchSize: config.BatchSize,
FlushInterval: config.FlushInterval,
},
provider.export,
)
if err != nil {
return nil, err
}
provider.server = server
return provider, nil
}
func (provider *provider) Start(ctx context.Context) error {
return provider.server.Start(ctx)
}
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
if event.PrincipalAttributes.PrincipalOrgID.IsZero() {
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
return
}
if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil {
return
}
provider.server.Add(ctx, event)
}
func (provider *provider) Healthy() <-chan struct{} {
return provider.server.Healthy()
}
func (provider *provider) Stop(ctx context.Context) error {
return provider.server.Stop(ctx)
}

View File

@@ -0,0 +1,52 @@
package otlphttpauditor
import (
"time"
"github.com/SigNoz/signoz/pkg/auditor"
client "github.com/SigNoz/signoz/pkg/http/client"
)
// retrier implements client.Retriable with exponential backoff
// derived from auditor.RetryConfig.
type retrier struct {
initialInterval time.Duration
maxInterval time.Duration
}
func newRetrier(cfg auditor.RetryConfig) *retrier {
return &retrier{
initialInterval: cfg.InitialInterval,
maxInterval: cfg.MaxInterval,
}
}
// NextInterval returns the backoff duration for the given retry attempt.
// Uses exponential backoff: initialInterval * 2^retry, capped at maxInterval.
func (r *retrier) NextInterval(retry int) time.Duration {
interval := r.initialInterval
for range retry {
interval *= 2
}
return min(interval, r.maxInterval)
}
func retrierOption(cfg auditor.RetryConfig) client.Option {
return client.WithRetriable(newRetrier(cfg))
}
func retryCountFromConfig(cfg auditor.RetryConfig) int {
if !cfg.Enabled || cfg.MaxElapsedTime <= 0 {
return 0
}
count := 0
elapsed := time.Duration(0)
interval := cfg.InitialInterval
for elapsed < cfg.MaxElapsedTime {
elapsed += interval
interval = min(interval*2, cfg.MaxInterval)
count++
}
return count
}

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

@@ -34,9 +34,22 @@ func (server *Server) Stop(ctx context.Context) error {
}
func (server *Server) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
subject := ""
switch claims.Principal {
case authtypes.PrincipalUser:
user, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil)
if err != nil {
return err
}
subject = user
case authtypes.PrincipalServiceAccount:
serviceAccount, err := authtypes.NewSubject(authtypes.TypeableServiceAccount, claims.ServiceAccountID, orgID, nil)
if err != nil {
return err
}
subject = serviceAccount
}
tupleSlice, err := typeable.Tuples(subject, relation, selectors, orgID)

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

@@ -13,7 +13,7 @@ var (
once sync.Once
)
// initializes the licensing configuration
// Config initializes the licensing configuration.
func Config(pollInterval time.Duration, failureThreshold int) licensing.Config {
once.Do(func() {
config = licensing.Config{PollInterval: pollInterval, FailureThreshold: failureThreshold}

View File

@@ -213,8 +213,8 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) MustGetTypeables() []authtypes.Typeable {

View File

@@ -65,7 +65,7 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
}
if anomalyQuery, ok := queryRangeRequest.IsAnomalyRequest(); ok {
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, queryRangeRequest)
anomalies, err := h.handleAnomalyQuery(ctx, orgID, anomalyQuery, &queryRangeRequest)
if err != nil {
render.Error(rw, errors.NewInternalf(errors.CodeInternal, "failed to get anomalies: %v", err))
return
@@ -79,7 +79,7 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
// Build step intervals from the anomaly query
stepIntervals := make(map[string]uint64)
if anomalyQuery.StepInterval.Duration > 0 {
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Duration.Seconds())
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Seconds())
}
finalResp := &qbtypes.QueryRangeResponse{
@@ -149,7 +149,7 @@ func (h *handler) createAnomalyProvider(seasonality anomalyV2.Seasonality) anoma
}
}
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
func (h *handler) handleAnomalyQuery(ctx context.Context, orgID valuer.UUID, anomalyQuery *qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation], queryRangeRequest *qbtypes.QueryRangeRequest) (*anomalyV2.AnomaliesResponse, error) {
seasonality := extractSeasonality(anomalyQuery)
provider := h.createAnomalyProvider(seasonality)

View File

@@ -14,10 +14,9 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -50,7 +49,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
apiKey, apiErr := ah.getOrCreateCloudIntegrationFactorAPIKey(r.Context(), valuer.MustNewUUID(claims.OrgID), cloudProvider)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't provision PAT for cloud integration:",
@@ -110,84 +109,44 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
ah.Respond(w, result)
}
func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) (
func (ah *APIHandler) getOrCreateCloudIntegrationFactorAPIKey(ctx context.Context, orgID valuer.UUID, cloudProvider string) (
string, *basemodel.ApiError,
) {
integrationPATName := fmt.Sprintf("%s integration", cloudProvider)
integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider)
integrationPATName := fmt.Sprintf("%s", cloudProvider)
serviceAccount, apiErr := ah.getOrCreateCloudIntegrationServiceAccount(ctx, orgID)
if apiErr != nil {
return "", apiErr
}
orgIdUUID, err := valuer.NewUUID(orgId)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't parse orgId: %w", err,
))
}
allPats, err := ah.Signoz.Modules.UserSetter.ListAPIKeys(ctx, orgIdUUID)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't list PATs: %w", err,
))
}
for _, p := range allPats {
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
return p.Token, nil
}
}
slog.InfoContext(ctx, "no PAT found for cloud integration, creating a new one",
"cloud_provider", cloudProvider,
)
newPAT, err := types.NewStorableAPIKey(
integrationPATName,
integrationUser.ID,
types.RoleViewer,
0,
)
factorAPIKey, err := serviceAccount.NewFactorAPIKey(integrationPATName, 0)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
}
err = ah.Signoz.Modules.UserSetter.CreateAPIKey(ctx, newPAT)
factorAPIKey, err = ah.Signoz.Modules.ServiceAccount.GetOrCreateFactorAPIKey(ctx, factorAPIKey)
if err != nil {
return "", basemodel.InternalError(fmt.Errorf(
"couldn't create cloud integration PAT: %w", err,
))
}
return newPAT.Token, nil
return factorAPIKey.Key, nil
}
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string,
) (*types.User, *basemodel.ApiError) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, valuer.MustNewUUID(orgId), types.UserStatusActive)
func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Context, orgId valuer.UUID) (*serviceaccounttypes.ServiceAccount, *basemodel.ApiError) {
domain := ah.Signoz.Modules.ServiceAccount.Config().Email.Domain
cloudIntegrationServiceAccount := serviceaccounttypes.NewServiceAccount("integration", domain, serviceaccounttypes.ServiceAccountStatusActive, orgId)
cloudIntegrationServiceAccount, err := ah.Signoz.Modules.ServiceAccount.GetOrCreate(ctx, orgId, cloudIntegrationServiceAccount)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.UserSetter.GetOrCreateUser(
ctx,
cloudIntegrationUser,
user.WithFactorPassword(password),
user.WithRoleNames([]string{authtypes.SigNozViewerRoleName}),
)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
}
return cloudIntegrationUser, nil
return cloudIntegrationServiceAccount, nil
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey 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"
@@ -48,7 +49,6 @@ import (
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
)
@@ -98,7 +98,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
)
rm, err := makeRulesManager(
reader,
signoz.Cache,
signoz.Alertmanager,
signoz.SQLStore,
@@ -106,6 +105,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,
@@ -227,7 +227,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
s.config.APIServer.Timeout.Default,
s.config.APIServer.Timeout.Max,
).Wrap)
r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap)
r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap)
r.Use(middleware.NewComment().Wrap)
apiHandler.RegisterRoutes(r, am)
@@ -240,7 +240,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
err := s.signoz.APIServer.AddToRouter(r)
@@ -344,28 +343,28 @@ 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(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(),
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

@@ -5,58 +5,34 @@ import (
"encoding/json"
"fmt"
"log/slog"
"math"
"strings"
"sync"
"time"
"github.com/SigNoz/signoz/ee/query-service/anomaly"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/common"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/units"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
querierV5 "github.com/SigNoz/signoz/pkg/querier"
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
"github.com/SigNoz/signoz/ee/anomaly"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
const (
RuleTypeAnomaly = "anomaly_rule"
)
type AnomalyRule struct {
*baserules.BaseRule
mtx sync.Mutex
reader interfaces.Reader
// querier is used for alerts migrated after the introduction of new query builder
querier querier.Querier
// querierV2 is used for alerts created after the introduction of new metrics query builder
querierV2 interfaces.Querier
// querierV5 is used for alerts migrated after the introduction of new query builder
querierV5 querierV5.Querier
provider anomaly.Provider
providerV2 anomalyV2.Provider
provider anomaly.Provider
version string
logger *slog.Logger
@@ -70,18 +46,16 @@ func NewAnomalyRule(
id string,
orgID valuer.UUID,
p *ruletypes.PostableRule,
reader interfaces.Reader,
querierV5 querierV5.Querier,
querier querier.Querier,
logger *slog.Logger,
cache cache.Cache,
opts ...baserules.RuleOption,
) (*AnomalyRule, error) {
logger.Info("creating new AnomalyRule", "rule_id", id)
logger.Info("creating new AnomalyRule", slog.String("rule.id", id))
opts = append(opts, baserules.WithLogger(logger))
baseRule, err := baserules.NewBaseRule(id, orgID, p, reader, opts...)
baseRule, err := baserules.NewBaseRule(id, orgID, p, opts...)
if err != nil {
return nil, err
}
@@ -101,93 +75,38 @@ func NewAnomalyRule(
t.seasonality = anomaly.SeasonalityDaily
}
logger.Info("using seasonality", "seasonality", t.seasonality.String())
logger.Info("using seasonality", slog.String("rule.id", id), slog.String("rule.seasonality", t.seasonality.StringValue()))
querierOptsV2 := querierV2.QuerierOptions{
Reader: reader,
Cache: cache,
KeyGenerator: queryBuilder.NewKeyGenerator(),
}
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
t.reader = reader
if t.seasonality == anomaly.SeasonalityHourly {
t.provider = anomaly.NewHourlyProvider(
anomaly.WithCache[*anomaly.HourlyProvider](cache),
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.HourlyProvider](reader),
anomaly.WithQuerier[*anomaly.HourlyProvider](querier),
anomaly.WithLogger[*anomaly.HourlyProvider](logger),
)
} else if t.seasonality == anomaly.SeasonalityDaily {
t.provider = anomaly.NewDailyProvider(
anomaly.WithCache[*anomaly.DailyProvider](cache),
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.DailyProvider](reader),
anomaly.WithQuerier[*anomaly.DailyProvider](querier),
anomaly.WithLogger[*anomaly.DailyProvider](logger),
)
} else if t.seasonality == anomaly.SeasonalityWeekly {
t.provider = anomaly.NewWeeklyProvider(
anomaly.WithCache[*anomaly.WeeklyProvider](cache),
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
anomaly.WithQuerier[*anomaly.WeeklyProvider](querier),
anomaly.WithLogger[*anomaly.WeeklyProvider](logger),
)
}
if t.seasonality == anomaly.SeasonalityHourly {
t.providerV2 = anomalyV2.NewHourlyProvider(
anomalyV2.WithQuerier[*anomalyV2.HourlyProvider](querierV5),
anomalyV2.WithLogger[*anomalyV2.HourlyProvider](logger),
)
} else if t.seasonality == anomaly.SeasonalityDaily {
t.providerV2 = anomalyV2.NewDailyProvider(
anomalyV2.WithQuerier[*anomalyV2.DailyProvider](querierV5),
anomalyV2.WithLogger[*anomalyV2.DailyProvider](logger),
)
} else if t.seasonality == anomaly.SeasonalityWeekly {
t.providerV2 = anomalyV2.NewWeeklyProvider(
anomalyV2.WithQuerier[*anomalyV2.WeeklyProvider](querierV5),
anomalyV2.WithLogger[*anomalyV2.WeeklyProvider](logger),
)
}
t.querierV5 = querierV5
t.querier = querier
t.version = p.Version
t.logger = logger
return &t, nil
}
func (r *AnomalyRule) Type() ruletypes.RuleType {
return RuleTypeAnomaly
return ruletypes.RuleTypeAnomaly
}
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v3.QueryRangeParamsV3, error) {
func (r *AnomalyRule) prepareQueryRange(ctx context.Context, ts time.Time) *qbtypes.QueryRangeRequest {
r.logger.InfoContext(
ctx, "prepare query range request v4", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds(),
)
st, en := r.Timestamps(ts)
start := st.UnixMilli()
end := en.UnixMilli()
compositeQuery := r.Condition().CompositeQuery
if compositeQuery.PanelType != v3.PanelTypeGraph {
compositeQuery.PanelType = v3.PanelTypeGraph
}
// default mode
return &v3.QueryRangeParamsV3{
Start: start,
End: end,
Step: int64(math.Max(float64(common.MinAllowedStepInterval(start, end)), 60)),
CompositeQuery: compositeQuery,
Variables: make(map[string]interface{}, 0),
NoCache: false,
}, nil
}
func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*qbtypes.QueryRangeRequest, error) {
r.logger.InfoContext(ctx, "prepare query range request v5", "ts", ts.UnixMilli(), "eval_window", r.EvalWindow().Milliseconds(), "eval_delay", r.EvalDelay().Milliseconds())
r.logger.InfoContext(ctx, "prepare query range request", slog.String("rule.id", r.ID()), slog.Int64("ts", ts.UnixMilli()), slog.Int64("eval.window_ms", r.EvalWindow().Milliseconds()), slog.Int64("eval.delay_ms", r.EvalDelay().Milliseconds()))
startTs, endTs := r.Timestamps(ts)
start, end := startTs.UnixMilli(), endTs.UnixMilli()
@@ -203,25 +122,14 @@ func (r *AnomalyRule) prepareQueryRangeV5(ctx context.Context, ts time.Time) (*q
}
req.CompositeQuery.Queries = make([]qbtypes.QueryEnvelope, len(r.Condition().CompositeQuery.Queries))
copy(req.CompositeQuery.Queries, r.Condition().CompositeQuery.Queries)
return req, nil
}
func (r *AnomalyRule) GetSelectedQuery() string {
return r.Condition().GetSelectedQueryName()
return req
}
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
params, err := r.prepareQueryRange(ctx, ts)
if err != nil {
return nil, err
}
err = r.PopulateTemporality(ctx, orgID, params)
if err != nil {
return nil, fmt.Errorf("internal error while setting temporality")
}
params := r.prepareQueryRange(ctx, ts)
anomalies, err := r.provider.GetAnomalies(ctx, orgID, &anomaly.GetAnomaliesRequest{
anomalies, err := r.provider.GetAnomalies(ctx, orgID, &anomaly.AnomaliesRequest{
Params: params,
Seasonality: r.seasonality,
})
@@ -229,87 +137,43 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
return nil, err
}
var queryResult *v3.Result
var queryResult *qbtypes.TimeSeriesData
for _, result := range anomalies.Results {
if result.QueryName == r.GetSelectedQuery() {
if result.QueryName == r.SelectedQuery(ctx) {
queryResult = result
break
}
}
hasData := len(queryResult.AnomalyScores) > 0
if queryResult == nil {
r.logger.WarnContext(ctx, "nil qb result", slog.String("rule.id", r.ID()), slog.Int64("ts", ts.UnixMilli()))
return ruletypes.Vector{}, nil
}
hasData := len(queryResult.Aggregations) > 0 &&
queryResult.Aggregations[0] != nil &&
len(queryResult.Aggregations[0].AnomalyScores) > 0
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
return ruletypes.Vector{*missingDataAlert}, nil
} else if !hasData {
r.logger.WarnContext(ctx, "no anomaly result", slog.String("rule.id", r.ID()))
return ruletypes.Vector{}, nil
}
var resultVector ruletypes.Vector
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
for _, series := range queryResult.AnomalyScores {
if !r.Condition().ShouldEval(series) {
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
continue
}
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})
if err != nil {
return nil, err
}
resultVector = append(resultVector, results...)
}
return resultVector, nil
}
func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, ts time.Time) (ruletypes.Vector, error) {
params, err := r.prepareQueryRangeV5(ctx, ts)
if err != nil {
return nil, err
}
anomalies, err := r.providerV2.GetAnomalies(ctx, orgID, &anomalyV2.AnomaliesRequest{
Params: *params,
Seasonality: anomalyV2.Seasonality{String: valuer.NewString(r.seasonality.String())},
})
if err != nil {
return nil, err
}
var qbResult *qbtypes.TimeSeriesData
for _, result := range anomalies.Results {
if result.QueryName == r.GetSelectedQuery() {
qbResult = result
break
}
}
if qbResult == nil {
r.logger.WarnContext(ctx, "nil qb result", "ts", ts.UnixMilli())
}
queryResult := transition.ConvertV5TimeSeriesDataToV4Result(qbResult)
hasData := len(queryResult.AnomalyScores) > 0
if missingDataAlert := r.HandleMissingDataAlert(ctx, ts, hasData); missingDataAlert != nil {
return ruletypes.Vector{*missingDataAlert}, nil
}
var resultVector ruletypes.Vector
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
scoresJSON, _ := json.Marshal(queryResult.Aggregations[0].AnomalyScores)
// TODO(srikanthccv): this could be noisy but we do this to answer false alert requests
r.logger.InfoContext(ctx, "anomaly scores", slog.String("rule.id", r.ID()), slog.String("anomaly.scores", string(scoresJSON)))
// Filter out new series if newGroupEvalDelay is configured
seriesToProcess := queryResult.AnomalyScores
seriesToProcess := queryResult.Aggregations[0].AnomalyScores
if r.ShouldSkipNewGroups() {
filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, seriesToProcess)
// In case of error we log the error and continue with the original series
if filterErr != nil {
r.logger.ErrorContext(ctx, "Error filtering new series, ", errors.Attr(filterErr), "rule_name", r.Name())
r.logger.ErrorContext(ctx, "error filtering new series", slog.String("rule.id", r.ID()), errors.Attr(filterErr))
} else {
seriesToProcess = filteredSeries
}
@@ -317,10 +181,10 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
for _, series := range seriesToProcess {
if !r.Condition().ShouldEval(series) {
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", slog.String("rule.id", r.ID()), slog.Int("series.num_points", len(series.Values)), slog.Int("series.required_points", r.Condition().RequiredNumPoints))
continue
}
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
results, err := r.Threshold.Eval(series, r.Unit(), ruletypes.EvalData{
ActiveAlerts: r.ActiveAlertsLabelFP(),
SendUnmatched: r.ShouldSendUnmatched(),
})
@@ -341,13 +205,9 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
var res ruletypes.Vector
var err error
if r.version == "v5" {
r.logger.InfoContext(ctx, "running v5 query")
res, err = r.buildAndRunQueryV5(ctx, r.OrgID(), ts)
} else {
r.logger.InfoContext(ctx, "running v4 query")
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
}
r.logger.InfoContext(ctx, "running query", slog.String("rule.id", r.ID()))
res, err = r.buildAndRunQuery(ctx, r.OrgID(), ts)
if err != nil {
return 0, err
}
@@ -371,7 +231,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
}
value := valueFormatter.Format(smpl.V, r.Unit())
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
r.logger.DebugContext(ctx, "alert template data for rule", slog.String("rule.id", r.ID()), slog.String("formatter.name", valueFormatter.Name()), slog.String("alert.value", value), slog.String("alert.threshold", threshold))
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
// Inject some convenience variables that are easier to remember for users
@@ -386,35 +246,34 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
defs+text,
"__alert_"+r.Name(),
tmplData,
times.Time(timestamp.FromTime(ts)),
nil,
)
result, err := tmpl.Expand()
if err != nil {
result = fmt.Sprintf("<error expanding template: %s>", err)
r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData, "rule_name", r.Name())
r.logger.ErrorContext(ctx, "expanding alert template failed", slog.String("rule.id", r.ID()), errors.Attr(err), slog.Any("alert.template_data", tmplData))
}
return result
}
lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel)
resultLabels := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels()
lb := ruletypes.NewBuilder(smpl.Metric...).Del(ruletypes.MetricNameLabel).Del(ruletypes.TemporalityLabel)
resultLabels := ruletypes.NewBuilder(smpl.Metric...).Del(ruletypes.MetricNameLabel).Del(ruletypes.TemporalityLabel).Labels()
for name, value := range r.Labels().Map() {
lb.Set(name, expand(value))
}
lb.Set(labels.AlertNameLabel, r.Name())
lb.Set(labels.AlertRuleIdLabel, r.ID())
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
lb.Set(ruletypes.AlertNameLabel, r.Name())
lb.Set(ruletypes.AlertRuleIDLabel, r.ID())
lb.Set(ruletypes.RuleSourceLabel, r.GeneratorURL())
annotations := make(labels.Labels, 0, len(r.Annotations().Map()))
annotations := make(ruletypes.Labels, 0, len(r.Annotations().Map()))
for name, value := range r.Annotations().Map() {
annotations = append(annotations, labels.Label{Name: name, Value: expand(value)})
annotations = append(annotations, ruletypes.Label{Name: name, Value: expand(value)})
}
if smpl.IsMissing {
lb.Set(labels.AlertNameLabel, "[No data] "+r.Name())
lb.Set(labels.NoDataLabel, "true")
lb.Set(ruletypes.AlertNameLabel, "[No data] "+r.Name())
lb.Set(ruletypes.NoDataLabel, "true")
}
lbs := lb.Labels()
@@ -422,17 +281,17 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
resultFPs[h] = struct{}{}
if _, ok := alerts[h]; ok {
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", "rule_id", r.ID(), "alert", alerts[h])
err = fmt.Errorf("duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
r.logger.ErrorContext(ctx, "the alert query returns duplicate records", slog.String("rule.id", r.ID()), slog.Any("alert", alerts[h]))
err = errors.NewInternalf(errors.CodeInternal, "duplicate alert found, vector contains metrics with the same labelset after applying alert labels")
return 0, err
}
alerts[h] = &ruletypes.Alert{
Labels: lbs,
QueryResultLables: resultLabels,
QueryResultLabels: resultLabels,
Annotations: annotations,
ActiveAt: ts,
State: model.StatePending,
State: ruletypes.StatePending,
Value: smpl.V,
GeneratorURL: r.GeneratorURL(),
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
@@ -441,12 +300,12 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
}
}
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
r.logger.InfoContext(ctx, "number of alerts found", slog.String("rule.id", r.ID()), slog.Int("alert.count", len(alerts)))
// alerts[h] is ready, add or update active list now
for h, a := range alerts {
// Check whether we already have alerting state for the identifying label set.
// Update the last value and annotations if so, create a new alert entry otherwise.
if alert, ok := r.Active[h]; ok && alert.State != model.StateInactive {
if alert, ok := r.Active[h]; ok && alert.State != ruletypes.StateInactive {
alert.Value = a.Value
alert.Annotations = a.Annotations
@@ -462,76 +321,76 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
r.Active[h] = a
}
itemsToAdd := []model.RuleStateHistory{}
itemsToAdd := []rulestatehistorytypes.RuleStateHistory{}
// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
for fp, a := range r.Active {
labelsJSON, err := json.Marshal(a.QueryResultLables)
labelsJSON, err := json.Marshal(a.QueryResultLabels)
if err != nil {
r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels)
r.logger.ErrorContext(ctx, "error marshaling labels", slog.String("rule.id", r.ID()), errors.Attr(err), slog.Any("alert.labels", a.Labels))
}
if _, ok := resultFPs[fp]; !ok {
// If the alert was previously firing, keep it around for a given
// retention time so it is reported as resolved to the AlertManager.
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
if a.State == ruletypes.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
delete(r.Active, fp)
}
if a.State != model.StateInactive {
a.State = model.StateInactive
if a.State != ruletypes.StateInactive {
a.State = ruletypes.StateInactive
a.ResolvedAt = ts
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
RuleID: r.ID(),
RuleName: r.Name(),
State: model.StateInactive,
State: ruletypes.StateInactive,
StateChanged: true,
UnixMilli: ts.UnixMilli(),
Labels: model.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLables.Hash(),
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLabels.Hash(),
Value: a.Value,
})
}
continue
}
if a.State == model.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
a.State = model.StateFiring
if a.State == ruletypes.StatePending && ts.Sub(a.ActiveAt) >= r.HoldDuration().Duration() {
a.State = ruletypes.StateFiring
a.FiredAt = ts
state := model.StateFiring
state := ruletypes.StateFiring
if a.Missing {
state = model.StateNoData
state = ruletypes.StateNoData
}
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
RuleID: r.ID(),
RuleName: r.Name(),
State: state,
StateChanged: true,
UnixMilli: ts.UnixMilli(),
Labels: model.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLables.Hash(),
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLabels.Hash(),
Value: a.Value,
})
}
// We need to change firing alert to recovering if the returned sample meets recovery threshold
changeFiringToRecovering := a.State == model.StateFiring && a.IsRecovering
changeFiringToRecovering := a.State == ruletypes.StateFiring && a.IsRecovering
// We need to change recovering alerts to firing if the returned sample meets target threshold
changeRecoveringToFiring := a.State == model.StateRecovering && !a.IsRecovering && !a.Missing
changeRecoveringToFiring := a.State == ruletypes.StateRecovering && !a.IsRecovering && !a.Missing
// in any of the above case we need to update the status of alert
if changeFiringToRecovering || changeRecoveringToFiring {
state := model.StateRecovering
state := ruletypes.StateRecovering
if changeRecoveringToFiring {
state = model.StateFiring
state = ruletypes.StateFiring
}
a.State = state
r.logger.DebugContext(ctx, "converting alert state", "name", r.Name(), "state", state)
itemsToAdd = append(itemsToAdd, model.RuleStateHistory{
r.logger.DebugContext(ctx, "converting alert state", slog.String("rule.id", r.ID()), slog.Any("alert.state", state))
itemsToAdd = append(itemsToAdd, rulestatehistorytypes.RuleStateHistory{
RuleID: r.ID(),
RuleName: r.Name(),
State: state,
StateChanged: true,
UnixMilli: ts.UnixMilli(),
Labels: model.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLables.Hash(),
Labels: rulestatehistorytypes.LabelsString(labelsJSON),
Fingerprint: a.QueryResultLabels.Hash(),
Value: a.Value,
})
}

View File

@@ -2,21 +2,19 @@ package rules
import (
"context"
"log/slog"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/ee/query-service/anomaly"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/ee/anomaly"
)
// mockAnomalyProvider is a mock implementation of anomaly.Provider for testing.
@@ -24,13 +22,13 @@ import (
// time periods (current, past period, current season, past season, past 2 seasons,
// past 3 seasons), making it cumbersome to create mock data.
type mockAnomalyProvider struct {
responses []*anomaly.GetAnomaliesResponse
responses []*anomaly.AnomaliesResponse
callCount int
}
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.GetAnomaliesRequest) (*anomaly.GetAnomaliesResponse, error) {
func (m *mockAnomalyProvider) GetAnomalies(ctx context.Context, orgID valuer.UUID, req *anomaly.AnomaliesRequest) (*anomaly.AnomaliesResponse, error) {
if m.callCount >= len(m.responses) {
return &anomaly.GetAnomaliesResponse{Results: []*v3.Result{}}, nil
return &anomaly.AnomaliesResponse{Results: []*qbtypes.TimeSeriesData{}}, nil
}
resp := m.responses[m.callCount]
m.callCount++
@@ -49,45 +47,46 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Test anomaly no data",
AlertType: ruletypes.AlertTypeMetric,
RuleType: RuleTypeAnomaly,
RuleType: ruletypes.RuleTypeAnomaly,
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: evalWindow,
Frequency: valuer.MustParseTextDuration("1m"),
}},
RuleCondition: &ruletypes.RuleCondition{
CompareOp: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
Target: &target,
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{
"A": {
QueryName: "A",
Expression: "A",
DataSource: v3.DataSourceMetrics,
Temporality: v3.Unspecified,
CompareOperator: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
Target: &target,
CompositeQuery: &ruletypes.AlertCompositeQuery{
QueryType: ruletypes.QueryTypeBuilder,
Queries: []qbtypes.QueryEnvelope{{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "A",
Signal: telemetrytypes.SignalMetrics,
},
},
}},
},
SelectedQuery: "A",
Seasonality: "daily",
Thresholds: &ruletypes.RuleThresholdData{
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{{
Name: "Test anomaly no data",
TargetValue: &target,
MatchType: ruletypes.AtleastOnce,
CompareOp: ruletypes.ValueIsAbove,
Name: "Test anomaly no data",
TargetValue: &target,
MatchType: ruletypes.AtleastOnce,
CompareOperator: ruletypes.ValueIsAbove,
}},
},
},
}
responseNoData := &anomaly.GetAnomaliesResponse{
Results: []*v3.Result{
responseNoData := &anomaly.AnomaliesResponse{
Results: []*qbtypes.TimeSeriesData{
{
QueryName: "A",
AnomalyScores: []*v3.Series{},
QueryName: "A",
Aggregations: []*qbtypes.AggregationBucket{{
AnomalyScores: []*qbtypes.TimeSeries{},
}},
},
},
}
@@ -115,23 +114,17 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
t.Run(c.description, func(t *testing.T) {
postableRule.RuleCondition.AlertOnAbsent = c.alertOnAbsent
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
options := clickhouseReader.NewOptions("primaryNamespace")
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
rule, err := NewAnomalyRule(
"test-anomaly-rule",
valuer.GenerateUUID(),
&postableRule,
reader,
nil,
logger,
nil,
)
require.NoError(t, err)
rule.provider = &mockAnomalyProvider{
responses: []*anomaly.GetAnomaliesResponse{responseNoData},
responses: []*anomaly.AnomaliesResponse{responseNoData},
}
alertsFound, err := rule.Eval(context.Background(), evalTime)
@@ -156,46 +149,47 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
postableRule := ruletypes.PostableRule{
AlertName: "Test anomaly no data with AbsentFor",
AlertType: ruletypes.AlertTypeMetric,
RuleType: RuleTypeAnomaly,
RuleType: ruletypes.RuleTypeAnomaly,
Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{
EvalWindow: evalWindow,
Frequency: valuer.MustParseTextDuration("1m"),
}},
RuleCondition: &ruletypes.RuleCondition{
CompareOp: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
AlertOnAbsent: true,
Target: &target,
CompositeQuery: &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
BuilderQueries: map[string]*v3.BuilderQuery{
"A": {
QueryName: "A",
Expression: "A",
DataSource: v3.DataSourceMetrics,
Temporality: v3.Unspecified,
CompareOperator: ruletypes.ValueIsAbove,
MatchType: ruletypes.AtleastOnce,
AlertOnAbsent: true,
Target: &target,
CompositeQuery: &ruletypes.AlertCompositeQuery{
QueryType: ruletypes.QueryTypeBuilder,
Queries: []qbtypes.QueryEnvelope{{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
Name: "A",
Signal: telemetrytypes.SignalMetrics,
},
},
}},
},
SelectedQuery: "A",
Seasonality: "daily",
Thresholds: &ruletypes.RuleThresholdData{
Kind: ruletypes.BasicThresholdKind,
Spec: ruletypes.BasicRuleThresholds{{
Name: "Test anomaly no data with AbsentFor",
TargetValue: &target,
MatchType: ruletypes.AtleastOnce,
CompareOp: ruletypes.ValueIsAbove,
Name: "Test anomaly no data with AbsentFor",
TargetValue: &target,
MatchType: ruletypes.AtleastOnce,
CompareOperator: ruletypes.ValueIsAbove,
}},
},
},
}
responseNoData := &anomaly.GetAnomaliesResponse{
Results: []*v3.Result{
responseNoData := &anomaly.AnomaliesResponse{
Results: []*qbtypes.TimeSeriesData{
{
QueryName: "A",
AnomalyScores: []*v3.Series{},
QueryName: "A",
Aggregations: []*qbtypes.AggregationBucket{{
AnomalyScores: []*qbtypes.TimeSeries{},
}},
},
},
}
@@ -229,32 +223,35 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
t1 := baseTime.Add(5 * time.Minute)
t2 := t1.Add(c.timeBetweenEvals)
responseWithData := &anomaly.GetAnomaliesResponse{
Results: []*v3.Result{
responseWithData := &anomaly.AnomaliesResponse{
Results: []*qbtypes.TimeSeriesData{
{
QueryName: "A",
AnomalyScores: []*v3.Series{
{
Labels: map[string]string{"test": "label"},
Points: []v3.Point{
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
Aggregations: []*qbtypes.AggregationBucket{{
AnomalyScores: []*qbtypes.TimeSeries{
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "Test"},
Value: "labels",
},
},
Values: []*qbtypes.TimeSeriesValue{
{Timestamp: baseTime.UnixMilli(), Value: 1.0},
{Timestamp: baseTime.Add(time.Minute).UnixMilli(), Value: 1.5},
},
},
},
},
}},
},
},
}
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
options := clickhouseReader.NewOptions("primaryNamespace")
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, nil, logger)
require.NoError(t, err)
rule.provider = &mockAnomalyProvider{
responses: []*anomaly.GetAnomaliesResponse{responseWithData, responseNoData},
responses: []*anomaly.AnomaliesResponse{responseWithData, responseNoData},
}
alertsFound1, err := rule.Eval(context.Background(), t1)

View File

@@ -11,9 +11,7 @@ import (
"github.com/google/uuid"
"github.com/SigNoz/signoz/pkg/errors"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -23,24 +21,25 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
rules := make([]baserules.Rule, 0)
var task baserules.Task
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
ruleID := baserules.RuleIDFromTaskName(opts.TaskName)
evaluation, err := opts.Rule.Evaluation.GetEvaluation()
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(
ruleId,
ruleID,
opts.OrgID,
opts.Rule,
opts.Reader,
opts.Querier,
opts.Logger,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -56,15 +55,15 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
// create promql rule
pr, err := baserules.NewPromRule(
ruleId,
ruleID,
opts.OrgID,
opts.Rule,
opts.Logger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -79,17 +78,16 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule
ar, err := NewAnomalyRule(
ruleId,
ruleID,
opts.OrgID,
opts.Rule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
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
@@ -101,7 +99,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
task = newTask(baserules.TaskTypeCh, opts.TaskName, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
} else {
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
}
return task, nil
@@ -109,12 +107,12 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
// TestNotification prepares a dummy rule for given rule parameters and
// sends a test notification. returns alert count and error (if any)
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.ApiError) {
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, error) {
ctx := context.Background()
if opts.Rule == nil {
return 0, basemodel.BadRequest(fmt.Errorf("rule is required"))
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "rule is required")
}
parsedRule := opts.Rule
@@ -134,15 +132,14 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
// add special labels for test alerts
parsedRule.Labels[labels.RuleSourceLabel] = ""
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
parsedRule.Labels[ruletypes.RuleSourceLabel] = ""
parsedRule.Labels[ruletypes.AlertRuleIDLabel] = ""
// create a threshold rule
rule, err = baserules.NewThresholdRule(
alertname,
opts.OrgID,
parsedRule,
opts.Reader,
opts.Querier,
opts.Logger,
baserules.WithSendAlways(),
@@ -154,7 +151,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
if err != nil {
slog.Error("failed to prepare a new threshold rule for test", "name", alertname, errors.Attr(err))
return 0, basemodel.BadRequest(err)
return 0, err
}
} else if parsedRule.RuleType == ruletypes.RuleTypeProm {
@@ -165,7 +162,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
opts.OrgID,
parsedRule,
opts.Logger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
@@ -176,7 +172,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
if err != nil {
slog.Error("failed to prepare a new promql rule for test", "name", alertname, errors.Attr(err))
return 0, basemodel.BadRequest(err)
return 0, err
}
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
// create anomaly rule
@@ -184,10 +180,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertname,
opts.OrgID,
parsedRule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.Cache,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -196,10 +190,10 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
)
if err != nil {
slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, errors.Attr(err))
return 0, basemodel.BadRequest(err)
return 0, err
}
} else {
return 0, basemodel.BadRequest(fmt.Errorf("failed to derive ruletype with given information"))
return 0, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to derive ruletype with given information")
}
// set timestamp to current utc time
@@ -208,7 +202,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertsFound, err := rule.Eval(ctx, ts)
if err != nil {
slog.Error("evaluating rule failed", "rule", rule.Name(), errors.Attr(err))
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
return 0, err
}
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)

View File

@@ -114,11 +114,8 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
},
})
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
if apiErr != nil {
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
}
require.Nil(t, apiErr)
count, err := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
require.Nil(t, err)
assert.Equal(t, tc.ExpectAlerts, count)
if tc.ExpectAlerts > 0 {
@@ -268,11 +265,8 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
},
})
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
if apiErr != nil {
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
}
require.Nil(t, apiErr)
count, err := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
require.Nil(t, err)
assert.Equal(t, tc.ExpectAlerts, count)
if tc.ExpectAlerts > 0 {

View File

@@ -336,9 +336,10 @@ func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldMo
}
fkReference := ""
if reference == Org {
switch reference {
case Org:
fkReference = OrgReference
} else if reference == User {
case User:
fkReference = UserReference
}
@@ -392,9 +393,10 @@ func (dialect *dialect) AddPrimaryKey(ctx context.Context, bun bun.IDB, oldModel
}
fkReference := ""
if reference == Org {
switch reference {
case Org:
fkReference = OrgReference
} else if reference == User {
case User:
fkReference = UserReference
}

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

@@ -19,7 +19,7 @@ var (
once sync.Once
)
// initializes the Zeus configuration
// initializes the Zeus configuration.
func Config() zeus.Config {
once.Do(func() {
parsedURL, err := neturl.Parse(url)

View File

@@ -189,7 +189,7 @@ func (provider *Provider) do(ctx context.Context, url *url.URL, method string, k
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
}
// This can be taken down to the client package
// This can be taken down to the client package.
func (provider *Provider) errFromStatusCode(statusCode int, errorMessage string) error {
switch statusCode {
case http.StatusBadRequest:

View File

@@ -205,6 +205,25 @@ module.exports = {
],
},
overrides: [
{
files: ['src/**/*.{jsx,tsx,ts}'],
excludedFiles: [
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/__tests__/**/*.{js,jsx,ts,tsx}',
],
rules: {
'no-restricted-properties': [
'error',
{
object: 'navigator',
property: 'clipboard',
message:
'Do not use navigator.clipboard directly since it does not work well with specific browsers. Use hook useCopyToClipboard from react-use library. https://streamich.github.io/react-use/?path=/story/side-effects-usecopytoclipboard--docs',
},
],
},
},
{
files: [
'**/*.test.{js,jsx,ts,tsx}',

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

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

@@ -101,6 +101,22 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
// Don't redirect to onboarding if workspace has issues (blocked, suspended, or restricted)
// User needs access to settings/billing to fix payment issues
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
const isWorkspaceAccessRestricted =
activeLicense?.state === LicenseState.TERMINATED ||
activeLicense?.state === LicenseState.EXPIRED ||
activeLicense?.state === LicenseState.CANCELLED;
const hasWorkspaceIssue =
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
if (hasWorkspaceIssue) {
return;
}
const isFirstUser = checkFirstTimeUser();
if (
isFirstUser &&
@@ -119,40 +135,36 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
orgPreferences,
usersData,
pathname,
trialInfo?.workSpaceBlock,
activeLicense?.state,
]);
const navigateToWorkSpaceBlocked = (route: any): void => {
const { path } = route;
const navigateToWorkSpaceBlocked = useCallback((): void => {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(path === ROUTES.SETTINGS ||
path === ROUTES.ORG_SETTINGS ||
path === ROUTES.MEMBERS_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
(pathname === ROUTES.SETTINGS ||
pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.BILLING ||
pathname === ROUTES.MY_SETTINGS);
if (
path &&
path !== ROUTES.WORKSPACE_LOCKED &&
pathname &&
pathname !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
};
}, [isAdmin, pathname]);
const navigateToWorkSpaceAccessRestricted = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
const navigateToWorkSpaceAccessRestricted = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
}
};
}, [pathname]);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
@@ -161,61 +173,53 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const { platform } = activeLicense;
if (
isWorkspaceAccessRestricted &&
platform === LicensePlatform.CLOUD &&
currentRoute
) {
navigateToWorkSpaceAccessRestricted(currentRoute);
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
navigateToWorkSpaceAccessRestricted();
}
}
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
}, [
isFetchingActiveLicense,
activeLicense,
navigateToWorkSpaceAccessRestricted,
]);
useEffect(() => {
if (!isFetchingActiveLicense) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
currentRoute &&
activeLicense?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked(currentRoute);
navigateToWorkSpaceBlocked();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
activeLicense?.platform,
mapRoutes,
pathname,
navigateToWorkSpaceBlocked,
]);
const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
const navigateToWorkSpaceSuspended = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED);
}
};
}, [pathname]);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicense.state === LicenseState.DEFAULTED;
if (
shouldSuspendWorkspace &&
currentRoute &&
activeLicense.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended(currentRoute);
navigateToWorkSpaceSuspended();
}
}
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
}, [isFetchingActiveLicense, activeLicense, navigateToWorkSpaceSuspended]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {

File diff suppressed because it is too large Load Diff

View File

@@ -157,10 +157,6 @@ export const IngestionSettings = Loadable(
() => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'),
);
export const APIKeys = Loadable(
() => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'),
);
export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
);

View File

@@ -513,6 +513,7 @@ export const oldRoutes = [
'/logs-save-views',
'/traces-save-views',
'/settings/access-tokens',
'/settings/api-keys',
'/messaging-queues',
'/alerts/edit',
];
@@ -523,7 +524,8 @@ export const oldNewRoutesMapping: Record<string, string> = {
'/logs-explorer/live': '/logs/logs-explorer/live',
'/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views',
'/settings/access-tokens': '/settings/api-keys',
'/settings/access-tokens': '/settings/service-accounts',
'/settings/api-keys': '/settings/service-accounts',
'/messaging-queues': '/messaging-queues/overview',
'/alerts/edit': '/alerts/overview',
};

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

@@ -31,10 +31,13 @@ import type {
GetMetricHighlightsPathParameters,
GetMetricMetadata200,
GetMetricMetadataPathParameters,
GetMetricsOnboardingStatus200,
GetMetricsStats200,
GetMetricsTreemap200,
InspectMetrics200,
ListMetrics200,
ListMetricsParams,
MetricsexplorertypesInspectMetricsRequestDTO,
MetricsexplorertypesStatsRequestDTO,
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
@@ -778,6 +781,176 @@ export const useUpdateMetricMetadata = <
return useMutation(mutationOptions);
};
/**
* Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.
* @summary Inspect raw metric data points
*/
export const inspectMetrics = (
metricsexplorertypesInspectMetricsRequestDTO: BodyType<MetricsexplorertypesInspectMetricsRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<InspectMetrics200>({
url: `/api/v2/metrics/inspect`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesInspectMetricsRequestDTO,
signal,
});
};
export const getInspectMetricsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
> => {
const mutationKey = ['inspectMetrics'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof inspectMetrics>>,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return inspectMetrics(data);
};
return { mutationFn, ...mutationOptions };
};
export type InspectMetricsMutationResult = NonNullable<
Awaited<ReturnType<typeof inspectMetrics>>
>;
export type InspectMetricsMutationBody = BodyType<MetricsexplorertypesInspectMetricsRequestDTO>;
export type InspectMetricsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Inspect raw metric data points
*/
export const useInspectMetrics = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
> => {
const mutationOptions = getInspectMetricsMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection
* @summary Check if non-SigNoz metrics have been received
*/
export const getMetricsOnboardingStatus = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMetricsOnboardingStatus200>({
url: `/api/v2/metrics/onboarding`,
method: 'GET',
signal,
});
};
export const getGetMetricsOnboardingStatusQueryKey = () => {
return [`/api/v2/metrics/onboarding`] as const;
};
export const getGetMetricsOnboardingStatusQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricsOnboardingStatusQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>
> = ({ signal }) => getMetricsOnboardingStatus(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricsOnboardingStatusQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>
>;
export type GetMetricsOnboardingStatusQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Check if non-SigNoz metrics have been received
*/
export function useGetMetricsOnboardingStatus<
TData = Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricsOnboardingStatusQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Check if non-SigNoz metrics have been received
*/
export const invalidateGetMetricsOnboardingStatus = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricsOnboardingStatusQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint provides list of metrics with their number of samples and timeseries for the given time range
* @summary Get metrics statistics

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

@@ -23,9 +23,15 @@ import type {
CreateServiceAccount201,
CreateServiceAccountKey201,
CreateServiceAccountKeyPathParameters,
CreateServiceAccountRole201,
CreateServiceAccountRolePathParameters,
DeleteServiceAccountPathParameters,
DeleteServiceAccountRolePathParameters,
GetMyServiceAccount200,
GetServiceAccount200,
GetServiceAccountPathParameters,
GetServiceAccountRoles200,
GetServiceAccountRolesPathParameters,
ListServiceAccountKeys200,
ListServiceAccountKeysPathParameters,
ListServiceAccounts200,
@@ -33,12 +39,10 @@ import type {
RevokeServiceAccountKeyPathParameters,
ServiceaccounttypesPostableFactorAPIKeyDTO,
ServiceaccounttypesPostableServiceAccountDTO,
ServiceaccounttypesPostableServiceAccountRoleDTO,
ServiceaccounttypesUpdatableFactorAPIKeyDTO,
ServiceaccounttypesUpdatableServiceAccountDTO,
ServiceaccounttypesUpdatableServiceAccountStatusDTO,
UpdateServiceAccountKeyPathParameters,
UpdateServiceAccountPathParameters,
UpdateServiceAccountStatusPathParameters,
} from '../sigNoz.schemas';
/**
@@ -399,13 +403,13 @@ export const invalidateGetServiceAccount = async (
*/
export const updateServiceAccount = (
{ id }: UpdateServiceAccountPathParameters,
serviceaccounttypesUpdatableServiceAccountDTO: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>,
serviceaccounttypesPostableServiceAccountDTO: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesUpdatableServiceAccountDTO,
data: serviceaccounttypesPostableServiceAccountDTO,
});
};
@@ -418,7 +422,7 @@ export const getUpdateServiceAccountMutationOptions = <
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
},
TContext
>;
@@ -427,7 +431,7 @@ export const getUpdateServiceAccountMutationOptions = <
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
},
TContext
> => {
@@ -444,7 +448,7 @@ export const getUpdateServiceAccountMutationOptions = <
Awaited<ReturnType<typeof updateServiceAccount>>,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -458,7 +462,7 @@ export const getUpdateServiceAccountMutationOptions = <
export type UpdateServiceAccountMutationResult = NonNullable<
Awaited<ReturnType<typeof updateServiceAccount>>
>;
export type UpdateServiceAccountMutationBody = BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
export type UpdateServiceAccountMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
export type UpdateServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -473,7 +477,7 @@ export const useUpdateServiceAccount = <
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
},
TContext
>;
@@ -482,7 +486,7 @@ export const useUpdateServiceAccount = <
TError,
{
pathParams: UpdateServiceAccountPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountDTO>;
data: BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
},
TContext
> => {
@@ -871,44 +875,150 @@ export const useUpdateServiceAccountKey = <
return useMutation(mutationOptions);
};
/**
* This endpoint updates an existing service account status
* @summary Updates a service account status
* This endpoint gets all the roles for the existing service account
* @summary Gets service account roles
*/
export const updateServiceAccountStatus = (
{ id }: UpdateServiceAccountStatusPathParameters,
serviceaccounttypesUpdatableServiceAccountStatusDTO: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>,
export const getServiceAccountRoles = (
{ id }: GetServiceAccountRolesPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/status`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesUpdatableServiceAccountStatusDTO,
return GeneratedAPIInstance<GetServiceAccountRoles200>({
url: `/api/v1/service_accounts/${id}/roles`,
method: 'GET',
signal,
});
};
export const getUpdateServiceAccountStatusMutationOptions = <
export const getGetServiceAccountRolesQueryKey = ({
id,
}: GetServiceAccountRolesPathParameters) => {
return [`/api/v1/service_accounts/${id}/roles`] as const;
};
export const getGetServiceAccountRolesQueryOptions = <
TData = Awaited<ReturnType<typeof getServiceAccountRoles>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetServiceAccountRolesPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRoles>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceAccountRolesQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getServiceAccountRoles>>
> = ({ signal }) => getServiceAccountRoles({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRoles>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetServiceAccountRolesQueryResult = NonNullable<
Awaited<ReturnType<typeof getServiceAccountRoles>>
>;
export type GetServiceAccountRolesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Gets service account roles
*/
export function useGetServiceAccountRoles<
TData = Awaited<ReturnType<typeof getServiceAccountRoles>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetServiceAccountRolesPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRoles>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceAccountRolesQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Gets service account roles
*/
export const invalidateGetServiceAccountRoles = async (
queryClient: QueryClient,
{ id }: GetServiceAccountRolesPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceAccountRolesQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint assigns a role to a service account
* @summary Create service account role
*/
export const createServiceAccountRole = (
{ id }: CreateServiceAccountRolePathParameters,
serviceaccounttypesPostableServiceAccountRoleDTO: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRole201>({
url: `/api/v1/service_accounts/${id}/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
> => {
const mutationKey = ['updateServiceAccountStatus'];
const mutationKey = ['createServiceAccountRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -918,52 +1028,299 @@ export const getUpdateServiceAccountStatusMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateServiceAccountStatus(pathParams, data);
return createServiceAccountRole(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateServiceAccountStatusMutationResult = NonNullable<
Awaited<ReturnType<typeof updateServiceAccountStatus>>
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
>;
export type UpdateServiceAccountStatusMutationBody = BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
export type UpdateServiceAccountStatusMutationError = ErrorType<RenderErrorResponseDTO>;
export type CreateServiceAccountRoleMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
export type CreateServiceAccountRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Updates a service account status
* @summary Create service account role
*/
export const useUpdateServiceAccountStatus = <
export const useCreateServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateServiceAccountStatus>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: UpdateServiceAccountStatusPathParameters;
data: BodyType<ServiceaccounttypesUpdatableServiceAccountStatusDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
> => {
const mutationOptions = getUpdateServiceAccountStatusMutationOptions(options);
const mutationOptions = getCreateServiceAccountRoleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint revokes a role from service account
* @summary Delete service account role
*/
export const deleteServiceAccountRole = ({
id,
rid,
}: DeleteServiceAccountRolePathParameters) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/roles/${rid}`,
method: 'DELETE',
});
};
export const getDeleteServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
>;
export type DeleteServiceAccountRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete service account role
*/
export const useDeleteServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationOptions = getDeleteServiceAccountRoleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint gets my service account
* @summary Gets my service account
*/
export const getMyServiceAccount = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMyServiceAccount200>({
url: `/api/v1/service_accounts/me`,
method: 'GET',
signal,
});
};
export const getGetMyServiceAccountQueryKey = () => {
return [`/api/v1/service_accounts/me`] as const;
};
export const getGetMyServiceAccountQueryOptions = <
TData = Awaited<ReturnType<typeof getMyServiceAccount>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMyServiceAccount>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetMyServiceAccountQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMyServiceAccount>>
> = ({ signal }) => getMyServiceAccount(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMyServiceAccount>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMyServiceAccountQueryResult = NonNullable<
Awaited<ReturnType<typeof getMyServiceAccount>>
>;
export type GetMyServiceAccountQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Gets my service account
*/
export function useGetMyServiceAccount<
TData = Awaited<ReturnType<typeof getMyServiceAccount>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMyServiceAccount>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMyServiceAccountQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Gets my service account
*/
export const invalidateGetMyServiceAccount = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMyServiceAccountQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint gets my service account
* @summary Updates my service account
*/
export const updateMyServiceAccount = (
serviceaccounttypesPostableServiceAccountDTO: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/me`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountDTO,
});
};
export const getUpdateMyServiceAccountMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMyServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMyServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
> => {
const mutationKey = ['updateMyServiceAccount'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateMyServiceAccount>>,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> }
> = (props) => {
const { data } = props ?? {};
return updateMyServiceAccount(data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateMyServiceAccountMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMyServiceAccount>>
>;
export type UpdateMyServiceAccountMutationBody = BodyType<ServiceaccounttypesPostableServiceAccountDTO>;
export type UpdateMyServiceAccountMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Updates my service account
*/
export const useUpdateMyServiceAccount = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMyServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateMyServiceAccount>>,
TError,
{ data: BodyType<ServiceaccounttypesPostableServiceAccountDTO> },
TContext
> => {
const mutationOptions = getUpdateMyServiceAccountMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -1363,6 +1363,32 @@ export interface GlobaltypesTokenizerConfigDTO {
enabled?: boolean;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type string
*/
metricName: string;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface MetricsexplorertypesInspectMetricsResponseDTO {
/**
* @type array
* @nullable true
*/
series: Querybuildertypesv5TimeSeriesDTO[] | null;
}
export interface MetricsexplorertypesListMetricDTO {
/**
* @type string
@@ -1508,6 +1534,13 @@ export interface MetricsexplorertypesMetricMetadataDTO {
unit: string;
}
export interface MetricsexplorertypesMetricsOnboardingResponseDTO {
/**
* @type boolean
*/
hasMetrics: boolean;
}
export interface MetricsexplorertypesStatDTO {
/**
* @type string
@@ -2677,7 +2710,140 @@ export interface RenderErrorResponseDTO {
status: string;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
export interface RulestatehistorytypesGettableRuleStateHistoryDTO {
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
overallState: RuletypesAlertStateDTO;
/**
* @type boolean
*/
overallStateChanged: boolean;
/**
* @type string
*/
ruleId: string;
/**
* @type string
*/
ruleName: string;
state: RuletypesAlertStateDTO;
/**
* @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: RuletypesAlertStateDTO;
}
export enum RuletypesAlertStateDTO {
inactive = 'inactive',
pending = 'pending',
recovering = 'recovering',
firing = 'firing',
nodata = 'nodata',
disabled = 'disabled',
}
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
/**
* @type string
* @format date-time
@@ -2692,10 +2858,6 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
* @type string
*/
id: string;
/**
* @type string
*/
key: string;
/**
* @type string
* @format date-time
@@ -2743,15 +2905,14 @@ export interface ServiceaccounttypesPostableServiceAccountDTO {
/**
* @type string
*/
email: string;
name: string;
}
export interface ServiceaccounttypesPostableServiceAccountRoleDTO {
/**
* @type string
*/
name: string;
/**
* @type array
*/
roles: string[];
id: string;
}
export interface ServiceaccounttypesServiceAccountDTO {
@@ -2760,11 +2921,65 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
email: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
deletedAt: Date;
updatedAt?: Date;
}
export interface ServiceaccounttypesServiceAccountRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
id: string;
role: AuthtypesRoleDTO;
/**
* @type string
*/
roleId: string;
/**
* @type string
*/
serviceAccountId: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface ServiceaccounttypesServiceAccountWithRolesDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
@@ -2783,8 +2998,9 @@ export interface ServiceaccounttypesServiceAccountDTO {
orgId: string;
/**
* @type array
* @nullable true
*/
roles: string[];
serviceAccountRoles: ServiceaccounttypesServiceAccountRoleDTO[] | null;
/**
* @type string
*/
@@ -2808,28 +3024,6 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
name: string;
}
export interface ServiceaccounttypesUpdatableServiceAccountDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
name: string;
/**
* @type array
*/
roles: string[];
}
export interface ServiceaccounttypesUpdatableServiceAccountStatusDTO {
/**
* @type string
*/
status: string;
}
export enum TelemetrytypesFieldContextDTO {
metric = 'metric',
log = 'log',
@@ -2973,63 +3167,6 @@ export interface TypesDeprecatedUserDTO {
updatedAt?: Date;
}
export interface TypesGettableAPIKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
createdByUser?: TypesUserDTO;
/**
* @type integer
* @format int64
*/
expiresAt?: number;
/**
* @type string
*/
id: string;
/**
* @type integer
* @format int64
*/
lastUsed?: number;
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
revoked?: boolean;
/**
* @type string
*/
role?: string;
/**
* @type string
*/
token?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
updatedByUser?: TypesUserDTO;
/**
* @type string
*/
userId?: string;
}
export interface TypesIdentifiableDTO {
/**
* @type string
@@ -3112,22 +3249,6 @@ export interface TypesOrganizationDTO {
updatedAt?: Date;
}
export interface TypesPostableAPIKeyDTO {
/**
* @type integer
* @format int64
*/
expiresInDays?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
role?: string;
}
export interface TypesPostableBulkInviteRequestDTO {
/**
* @type array
@@ -3207,51 +3328,6 @@ export interface TypesResetPasswordTokenDTO {
token?: string;
}
export interface TypesStorableAPIKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name?: string;
/**
* @type boolean
*/
revoked?: boolean;
/**
* @type string
*/
role?: string;
/**
* @type string
*/
token?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
/**
* @type string
*/
userId?: string;
}
export interface TypesUpdatableUserDTO {
/**
* @type string
@@ -3790,31 +3866,6 @@ export type GetOrgPreference200 = {
export type UpdateOrgPreferencePathParameters = {
name: string;
};
export type ListAPIKeys200 = {
/**
* @type array
*/
data: TypesGettableAPIKeyDTO[];
/**
* @type string
*/
status: string;
};
export type CreateAPIKey201 = {
data: TypesGettableAPIKeyDTO;
/**
* @type string
*/
status: string;
};
export type RevokeAPIKeyPathParameters = {
id: string;
};
export type UpdateAPIKeyPathParameters = {
id: string;
};
export type GetPublicDashboardDataPathParameters = {
id: string;
};
@@ -3919,7 +3970,7 @@ export type GetServiceAccountPathParameters = {
id: string;
};
export type GetServiceAccount200 = {
data: ServiceaccounttypesServiceAccountDTO;
data: ServiceaccounttypesServiceAccountWithRolesDTO;
/**
* @type string
*/
@@ -3936,7 +3987,7 @@ export type ListServiceAccountKeys200 = {
/**
* @type array
*/
data: ServiceaccounttypesFactorAPIKeyDTO[];
data: ServiceaccounttypesGettableFactorAPIKeyDTO[];
/**
* @type string
*/
@@ -3962,9 +4013,44 @@ export type UpdateServiceAccountKeyPathParameters = {
id: string;
fid: string;
};
export type UpdateServiceAccountStatusPathParameters = {
export type GetServiceAccountRolesPathParameters = {
id: string;
};
export type GetServiceAccountRoles200 = {
/**
* @type array
* @nullable true
*/
data: AuthtypesRoleDTO[] | null;
/**
* @type string
*/
status: string;
};
export type CreateServiceAccountRolePathParameters = {
id: string;
};
export type CreateServiceAccountRole201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteServiceAccountRolePathParameters = {
id: string;
rid: string;
};
export type GetMyServiceAccount200 = {
data: ServiceaccounttypesServiceAccountWithRolesDTO;
/**
* @type string
*/
status: string;
};
export type ListUsersDeprecated200 = {
/**
* @type array
@@ -4258,6 +4344,22 @@ export type GetMetricMetadata200 = {
export type UpdateMetricMetadataPathParameters = {
metricName: string;
};
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricsOnboardingStatus200 = {
data: MetricsexplorertypesMetricsOnboardingResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricsStats200 = {
data: MetricsexplorertypesStatsResponseDTO;
/**
@@ -4312,6 +4414,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?: RuletypesAlertStateDTO;
/**
* @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

@@ -21,7 +21,6 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
ChangePasswordPathParameters,
CreateAPIKey201,
CreateInvite201,
DeleteUserPathParameters,
GetMyUser200,
@@ -36,24 +35,19 @@ import type {
GetUserPathParameters,
GetUsersByRoleID200,
GetUsersByRoleIDPathParameters,
ListAPIKeys200,
ListUsers200,
ListUsersDeprecated200,
RemoveUserRoleByUserIDAndRoleIDPathParameters,
RenderErrorResponseDTO,
RevokeAPIKeyPathParameters,
SetRoleByUserIDPathParameters,
TypesChangePasswordRequestDTO,
TypesDeprecatedUserDTO,
TypesPostableAPIKeyDTO,
TypesPostableBulkInviteRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesPostableRoleDTO,
TypesStorableAPIKeyDTO,
TypesUpdatableUserDTO,
UpdateAPIKeyPathParameters,
UpdateUserDeprecated200,
UpdateUserDeprecatedPathParameters,
UpdateUserPathParameters,
@@ -428,349 +422,6 @@ export const useCreateBulkInvite = <
return useMutation(mutationOptions);
};
/**
* This endpoint lists all api keys
* @summary List api keys
*/
export const listAPIKeys = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListAPIKeys200>({
url: `/api/v1/pats`,
method: 'GET',
signal,
});
};
export const getListAPIKeysQueryKey = () => {
return [`/api/v1/pats`] as const;
};
export const getListAPIKeysQueryOptions = <
TData = Awaited<ReturnType<typeof listAPIKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListAPIKeysQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listAPIKeys>>> = ({
signal,
}) => listAPIKeys(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListAPIKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof listAPIKeys>>
>;
export type ListAPIKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List api keys
*/
export function useListAPIKeys<
TData = Awaited<ReturnType<typeof listAPIKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listAPIKeys>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListAPIKeysQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List api keys
*/
export const invalidateListAPIKeys = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListAPIKeysQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates an api key
* @summary Create api key
*/
export const createAPIKey = (
typesPostableAPIKeyDTO: BodyType<TypesPostableAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAPIKey201>({
url: `/api/v1/pats`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableAPIKeyDTO,
signal,
});
};
export const getCreateAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
> => {
const mutationKey = ['createAPIKey'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createAPIKey>>,
{ data: BodyType<TypesPostableAPIKeyDTO> }
> = (props) => {
const { data } = props ?? {};
return createAPIKey(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof createAPIKey>>
>;
export type CreateAPIKeyMutationBody = BodyType<TypesPostableAPIKeyDTO>;
export type CreateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create api key
*/
export const useCreateAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
TContext
> => {
const mutationOptions = getCreateAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint revokes an api key
* @summary Revoke api key
*/
export const revokeAPIKey = ({ id }: RevokeAPIKeyPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/pats/${id}`,
method: 'DELETE',
});
};
export const getRevokeAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
> => {
const mutationKey = ['revokeAPIKey'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof revokeAPIKey>>,
{ pathParams: RevokeAPIKeyPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return revokeAPIKey(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RevokeAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof revokeAPIKey>>
>;
export type RevokeAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Revoke api key
*/
export const useRevokeAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof revokeAPIKey>>,
TError,
{ pathParams: RevokeAPIKeyPathParameters },
TContext
> => {
const mutationOptions = getRevokeAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint updates an api key
* @summary Update api key
*/
export const updateAPIKey = (
{ id }: UpdateAPIKeyPathParameters,
typesStorableAPIKeyDTO: BodyType<TypesStorableAPIKeyDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/pats/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesStorableAPIKeyDTO,
});
};
export const getUpdateAPIKeyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
> => {
const mutationKey = ['updateAPIKey'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateAPIKey>>,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateAPIKey(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAPIKey>>
>;
export type UpdateAPIKeyMutationBody = BodyType<TypesStorableAPIKeyDTO>;
export type UpdateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update api key
*/
export const useUpdateAPIKey = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateAPIKey>>,
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateAPIKeyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint resets the password by token
* @summary Reset password

View File

@@ -1,54 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface InspectMetricsRequest {
metricName: string;
start: number;
end: number;
filters: TagFilter;
}
export interface InspectMetricsResponse {
status: string;
data: {
series: InspectMetricsSeries[];
};
}
export interface InspectMetricsSeries {
title?: string;
strokeColor?: string;
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: InspectMetricsTimestampValue[];
}
interface InspectMetricsTimestampValue {
timestamp: number;
value: string;
}
export const getInspectMetricsDetails = async (
request: InspectMetricsRequest,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<InspectMetricsResponse> | ErrorResponse> => {
try {
const response = await axios.post(`/metrics/inspect`, request, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,75 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricType } from './getMetricsList';
export interface MetricDetails {
name: string;
description: string;
type: string;
unit: string;
timeseries: number;
samples: number;
timeSeriesTotal: number;
timeSeriesActive: number;
lastReceived: string;
attributes: MetricDetailsAttribute[] | null;
metadata?: {
metric_type: MetricType;
description: string;
unit: string;
temporality?: Temporality;
};
alerts: MetricDetailsAlert[] | null;
dashboards: MetricDetailsDashboard[] | null;
}
export enum Temporality {
CUMULATIVE = 'Cumulative',
DELTA = 'Delta',
}
export interface MetricDetailsAttribute {
key: string;
value: string[];
valueCount: number;
}
export interface MetricDetailsAlert {
alert_name: string;
alert_id: string;
}
export interface MetricDetailsDashboard {
dashboard_name: string;
dashboard_id: string;
}
export interface MetricDetailsResponse {
status: string;
data: MetricDetails;
}
export const getMetricDetails = async (
metricName: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricDetailsResponse> | ErrorResponse> => {
try {
const response = await axios.get(`/metrics/${metricName}/metadata`, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,67 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import {
OrderByPayload,
TreemapViewType,
} from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: OrderByPayload;
}
export enum MetricType {
SUM = 'Sum',
GAUGE = 'Gauge',
HISTOGRAM = 'Histogram',
SUMMARY = 'Summary',
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
export interface MetricsListItemData {
metric_name: string;
description: string;
type: MetricType;
unit: string;
[TreemapViewType.TIMESERIES]: number;
[TreemapViewType.SAMPLES]: number;
lastReceived: string;
}
export interface MetricsListResponse {
status: string;
data: {
metrics: MetricsListItemData[];
total?: number;
};
}
export const getMetricsList = async (
props: MetricsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,44 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export interface MetricsListFilterKeysResponse {
status: string;
data: {
metricColumns: string[];
attributeKeys: BaseAutocompleteData[];
};
}
export interface GetMetricsListFilterKeysParams {
searchText: string;
limit?: number;
}
export const getMetricsListFilterKeys = async (
params: GetMetricsListFilterKeysParams,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse> => {
try {
const response = await axios.get('/metrics/filters/keys', {
params: {
searchText: params.searchText,
limit: params.limit,
},
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,43 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface MetricsListFilterValuesPayload {
filterAttributeKeyDataType: string;
filterKey: string;
searchText: string;
limit: number;
}
export interface MetricsListFilterValuesResponse {
status: string;
data: {
filterValues: string[];
};
}
export const getMetricsListFilterValues = async (
props: MetricsListFilterValuesPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/metrics/filters/values', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,60 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export interface RelatedMetricsPayload {
start: number;
end: number;
currentMetricName: string;
}
export interface RelatedMetricDashboard {
dashboard_name: string;
dashboard_id: string;
widget_id: string;
widget_name: string;
}
export interface RelatedMetricAlert {
alert_name: string;
alert_id: string;
}
export interface RelatedMetric {
name: string;
query: IBuilderQuery;
dashboards: RelatedMetricDashboard[];
alerts: RelatedMetricAlert[];
}
export interface RelatedMetricsResponse {
status: 'success';
data: {
related_metrics: RelatedMetric[];
};
}
export const getRelatedMetrics = async (
props: RelatedMetricsPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<RelatedMetricsResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/related', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
APIKeyProps,
CreateAPIKeyProps,
CreatePayloadProps,
} from 'types/api/pat/types';
const create = async (
props: CreateAPIKeyProps,
): Promise<SuccessResponseV2<APIKeyProps>> => {
try {
const response = await axios.post<CreatePayloadProps>('/pats', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default create;

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
const deleteAPIKey = async (id: string): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete(`/pats/${id}`);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteAPIKey;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { AllAPIKeyProps, APIKeyProps } from 'types/api/pat/types';
const list = async (): Promise<SuccessResponseV2<APIKeyProps[]>> => {
try {
const response = await axios.get<AllAPIKeyProps>('/pats');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdateAPIKeyProps } from 'types/api/pat/types';
const updateAPIKey = async (
props: UpdateAPIKeyProps,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/pats/${props.id}`, {
...props.data,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default updateAPIKey;

View File

@@ -1,97 +0,0 @@
.announcement-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-4);
padding: var(--padding-2) var(--padding-4);
height: 40px;
font-family: var(--font-sans), sans-serif;
font-size: var(--label-base-500-font-size);
line-height: var(--label-base-500-line-height);
font-weight: var(--label-base-500-font-weight);
letter-spacing: -0.065px;
&--warning {
background-color: var(--callout-warning-background);
color: var(--callout-warning-description);
.announcement-banner__action,
.announcement-banner__dismiss {
background: var(--callout-warning-border);
}
}
&--info {
background-color: var(--callout-primary-background);
color: var(--callout-primary-description);
.announcement-banner__action,
.announcement-banner__dismiss {
background: var(--callout-primary-border);
}
}
&--error {
background-color: var(--callout-error-background);
color: var(--callout-error-description);
.announcement-banner__action,
.announcement-banner__dismiss {
background: var(--callout-error-border);
}
}
&--success {
background-color: var(--callout-success-background);
color: var(--callout-success-description);
.announcement-banner__action,
.announcement-banner__dismiss {
background: var(--callout-success-border);
}
}
&__body {
display: flex;
align-items: center;
gap: var(--spacing-4);
flex: 1;
min-width: 0;
}
&__icon {
display: flex;
align-items: center;
flex-shrink: 0;
}
&__message {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: var(--line-height-normal);
strong {
font-weight: var(--font-weight-semibold);
}
}
&__action {
height: 24px;
font-size: var(--label-small-500-font-size);
color: currentColor;
&:hover {
opacity: 0.8;
}
}
&__dismiss {
width: 24px;
height: 24px;
padding: 0;
color: currentColor;
&:hover {
opacity: 0.8;
}
}
}

View File

@@ -1,89 +0,0 @@
import { render, screen, userEvent } from 'tests/test-utils';
import {
AnnouncementBanner,
AnnouncementBannerProps,
PersistedAnnouncementBanner,
} from './index';
const STORAGE_KEY = 'test-banner-dismissed';
function renderBanner(props: Partial<AnnouncementBannerProps> = {}): void {
render(<AnnouncementBanner message="Test message" {...props} />);
}
afterEach(() => {
localStorage.removeItem(STORAGE_KEY);
});
describe('AnnouncementBanner', () => {
it('renders message and default warning variant', () => {
renderBanner({ message: <strong>Heads up</strong> });
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('announcement-banner--warning');
expect(alert).toHaveTextContent('Heads up');
});
it.each(['warning', 'info', 'success', 'error'] as const)(
'renders %s variant correctly',
(type) => {
renderBanner({ type, message: 'Test message' });
const alert = screen.getByRole('alert');
expect(alert).toHaveClass(`announcement-banner--${type}`);
},
);
it('calls action onClick when action button is clicked', async () => {
const onClick = jest.fn() as jest.MockedFunction<() => void>;
renderBanner({ action: { label: 'Go to Settings', onClick } });
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /go to settings/i }));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('hides dismiss button when onClose is not provided and hides icon when icon is null', () => {
renderBanner({ onClose: undefined, icon: null });
expect(
screen.queryByRole('button', { name: /dismiss/i }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('alert')?.querySelector('.announcement-banner__icon'),
).not.toBeInTheDocument();
});
});
describe('PersistedAnnouncementBanner', () => {
it('dismisses on click, calls onDismiss, and persists to localStorage', async () => {
const onDismiss = jest.fn() as jest.MockedFunction<() => void>;
render(
<PersistedAnnouncementBanner
message="Test message"
storageKey={STORAGE_KEY}
onDismiss={onDismiss}
/>,
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
await user.click(screen.getByRole('button', { name: /dismiss/i }));
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(onDismiss).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(STORAGE_KEY)).toBe('true');
});
it('does not render when storageKey is already set in localStorage', () => {
localStorage.setItem(STORAGE_KEY, 'true');
render(
<PersistedAnnouncementBanner
message="Test message"
storageKey={STORAGE_KEY}
/>,
);
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});

View File

@@ -1,84 +0,0 @@
import { ReactNode } from 'react';
import { Button } from '@signozhq/button';
import {
CircleAlert,
CircleCheckBig,
Info,
TriangleAlert,
X,
} from '@signozhq/icons';
import cx from 'classnames';
import './AnnouncementBanner.styles.scss';
export type AnnouncementBannerType = 'warning' | 'info' | 'error' | 'success';
export interface AnnouncementBannerAction {
label: string;
onClick: () => void;
}
export interface AnnouncementBannerProps {
message: ReactNode;
type?: AnnouncementBannerType;
icon?: ReactNode | null;
action?: AnnouncementBannerAction;
onClose?: () => void;
className?: string;
}
const DEFAULT_ICONS: Record<AnnouncementBannerType, ReactNode> = {
warning: <TriangleAlert size={14} />,
info: <Info size={14} />,
error: <CircleAlert size={14} />,
success: <CircleCheckBig size={14} />,
};
export default function AnnouncementBanner({
message,
type = 'warning',
icon,
action,
onClose,
className,
}: AnnouncementBannerProps): JSX.Element {
const resolvedIcon = icon === null ? null : icon ?? DEFAULT_ICONS[type];
return (
<div
role="alert"
className={cx(
'announcement-banner',
`announcement-banner--${type}`,
className,
)}
>
<div className="announcement-banner__body">
{resolvedIcon && (
<span className="announcement-banner__icon">{resolvedIcon}</span>
)}
<span className="announcement-banner__message">{message}</span>
{action && (
<Button
type="button"
className="announcement-banner__action"
onClick={action.onClick}
>
{action.label}
</Button>
)}
</div>
{onClose && (
<Button
type="button"
aria-label="Dismiss"
className="announcement-banner__dismiss"
onClick={onClose}
>
<X size={14} />
</Button>
)}
</div>
);
}

View File

@@ -1,34 +0,0 @@
import { useState } from 'react';
import AnnouncementBanner, {
AnnouncementBannerProps,
} from './AnnouncementBanner';
interface PersistedAnnouncementBannerProps extends AnnouncementBannerProps {
storageKey: string;
onDismiss?: () => void;
}
function isDismissed(storageKey: string): boolean {
return localStorage.getItem(storageKey) === 'true';
}
export default function PersistedAnnouncementBanner({
storageKey,
onDismiss,
...props
}: PersistedAnnouncementBannerProps): JSX.Element | null {
const [visible, setVisible] = useState(() => !isDismissed(storageKey));
if (!visible) {
return null;
}
const handleClose = (): void => {
localStorage.setItem(storageKey, 'true');
setVisible(false);
onDismiss?.();
};
return <AnnouncementBanner {...props} onClose={handleClose} />;
}

View File

@@ -1,12 +0,0 @@
import AnnouncementBanner from './AnnouncementBanner';
import PersistedAnnouncementBanner from './PersistedAnnouncementBanner';
export type {
AnnouncementBannerAction,
AnnouncementBannerProps,
AnnouncementBannerType,
} from './AnnouncementBanner';
export { AnnouncementBanner, PersistedAnnouncementBanner };
export default AnnouncementBanner;

View File

@@ -12,17 +12,13 @@ import {
} from 'api/generated/services/serviceaccount';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import RolesSelect, { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { EMAIL_REGEX } from 'utils/app';
import './CreateServiceAccountModal.styles.scss';
interface FormValues {
name: string;
email: string;
roles: string[];
}
function CreateServiceAccountModal(): JSX.Element {
@@ -41,8 +37,6 @@ function CreateServiceAccountModal(): JSX.Element {
mode: 'onChange',
defaultValues: {
name: '',
email: '',
roles: [],
},
});
@@ -70,13 +64,6 @@ function CreateServiceAccountModal(): JSX.Element {
},
},
});
const {
roles,
isLoading: rolesLoading,
isError: rolesError,
error: rolesErrorObj,
refetch: refetchRoles,
} = useRoles();
function handleClose(): void {
reset();
@@ -87,8 +74,6 @@ function CreateServiceAccountModal(): JSX.Element {
createServiceAccount({
data: {
name: values.name.trim(),
email: values.email.trim(),
roles: values.roles,
},
});
}
@@ -134,68 +119,6 @@ function CreateServiceAccountModal(): JSX.Element {
<p className="create-sa-form__error">{errors.name.message}</p>
)}
</div>
<div className="create-sa-form__item">
<label htmlFor="sa-email">Email Address</label>
<Controller
name="email"
control={control}
rules={{
required: 'Email Address is required',
pattern: {
value: EMAIL_REGEX,
message: 'Please enter a valid email address',
},
}}
render={({ field }): JSX.Element => (
<Input
id="sa-email"
type="email"
placeholder="email@example.com"
className="create-sa-form__input"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
/>
{errors.email && (
<p className="create-sa-form__error">{errors.email.message}</p>
)}
</div>
<p className="create-sa-form__helper">
Used only for notifications about this service account. It is not used for
authentication.
</p>
<div className="create-sa-form__item">
<label htmlFor="sa-roles">Roles</label>
<Controller
name="roles"
control={control}
rules={{
validate: (value): string | true =>
value.length > 0 || 'At least one role is required',
}}
render={({ field }): JSX.Element => (
<RolesSelect
id="sa-roles"
mode="multiple"
roles={roles}
loading={rolesLoading}
isError={rolesError}
error={rolesErrorObj}
onRefetch={refetchRoles}
placeholder="Select roles"
value={field.value}
onChange={field.onChange}
/>
)}
/>
{errors.roles && (
<p className="create-sa-form__error">{errors.roles.message}</p>
)}
</div>
</form>
</div>

View File

@@ -1,5 +1,4 @@
import { toast } from '@signozhq/sonner';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -12,7 +11,6 @@ jest.mock('@signozhq/sonner', () => ({
const mockToast = jest.mocked(toast);
const ROLES_ENDPOINT = '*/api/v1/roles';
const SERVICE_ACCOUNTS_ENDPOINT = '*/api/v1/service_accounts';
function renderModal(): ReturnType<typeof render> {
@@ -27,9 +25,6 @@ describe('CreateServiceAccountModal', () => {
beforeEach(() => {
jest.clearAllMocks();
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: {} })),
),
@@ -48,38 +43,11 @@ describe('CreateServiceAccountModal', () => {
).toBeDisabled();
});
it('submit button remains disabled when email is invalid', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await user.type(screen.getByPlaceholderText('Enter a name'), 'My Bot');
await user.type(
screen.getByPlaceholderText('email@example.com'),
'not-an-email',
);
await user.click(screen.getByText('Select roles'));
await user.click(await screen.findByTitle('signoz-admin'));
await waitFor(() =>
expect(
screen.getByRole('button', { name: /Create Service Account/i }),
).toBeDisabled(),
);
});
it('successful submit shows toast.success and closes modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await user.type(screen.getByPlaceholderText('Enter a name'), 'Deploy Bot');
await user.type(
screen.getByPlaceholderText('email@example.com'),
'deploy@acme.io',
);
await user.click(screen.getByText('Select roles'));
await user.click(await screen.findByTitle('signoz-admin'));
const submitBtn = screen.getByRole('button', {
name: /Create Service Account/i,
@@ -116,13 +84,6 @@ describe('CreateServiceAccountModal', () => {
renderModal();
await user.type(screen.getByPlaceholderText('Enter a name'), 'Dupe Bot');
await user.type(
screen.getByPlaceholderText('email@example.com'),
'dupe@acme.io',
);
await user.click(screen.getByText('Select roles'));
await user.click(await screen.findByTitle('signoz-admin'));
const submitBtn = screen.getByRole('button', {
name: /Create Service Account/i,
@@ -164,16 +125,4 @@ describe('CreateServiceAccountModal', () => {
await screen.findByText('Name is required');
});
it('shows "Please enter a valid email address" for a malformed email', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
await user.type(
screen.getByPlaceholderText('email@example.com'),
'not-an-email',
);
await screen.findByText('Please enter a valid email address');
});
});

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Badge } from '@signozhq/badge';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
@@ -177,26 +178,30 @@ function EditMemberDrawer({
}
}, [member, isInvited, setLinkType, onClose]);
const [copyState, copyToClipboard] = useCopyToClipboard();
const handleCopyResetLink = useCallback(async (): Promise<void> => {
if (!resetLink) {
return;
}
try {
await navigator.clipboard.writeText(resetLink);
setHasCopiedResetLink(true);
setTimeout(() => setHasCopiedResetLink(false), 2000);
toast.success(
linkType === 'invite'
? 'Invite link copied to clipboard'
: 'Reset link copied to clipboard',
{ richColors: true },
);
} catch {
copyToClipboard(resetLink);
setHasCopiedResetLink(true);
setTimeout(() => setHasCopiedResetLink(false), 2000);
toast.success(
linkType === 'invite'
? 'Invite link copied to clipboard'
: 'Reset link copied to clipboard',
{ richColors: true },
);
}, [resetLink, copyToClipboard, linkType]);
useEffect(() => {
if (copyState.error) {
toast.error('Failed to copy link', {
richColors: true,
});
}
}, [resetLink, linkType]);
}, [copyState.error]);
const handleClose = useCallback((): void => {
setShowDeleteConfirm(false);

View File

@@ -7,13 +7,7 @@ import {
useUpdateUserDeprecated,
} from 'api/generated/services/users';
import { MemberStatus } from 'container/MembersSettings/utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { ROLES } from 'types/roles';
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
@@ -65,6 +59,16 @@ jest.mock('@signozhq/sonner', () => ({
},
}));
const mockCopyToClipboard = jest.fn();
const mockCopyState = { value: undefined, error: undefined };
jest.mock('react-use', () => ({
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
mockCopyState,
mockCopyToClipboard,
],
}));
const mockUpdateMutate = jest.fn();
const mockDeleteMutate = jest.fn();
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
@@ -361,32 +365,14 @@ describe('EditMemberDrawer', () => {
});
describe('Generate Password Reset Link', () => {
const mockWriteText = jest.fn().mockResolvedValue(undefined);
let clipboardSpy: jest.SpyInstance | undefined;
beforeAll(() => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: (): Promise<void> => Promise.resolve() },
configurable: true,
writable: true,
});
});
beforeEach(() => {
mockWriteText.mockClear();
clipboardSpy = jest
.spyOn(navigator.clipboard, 'writeText')
.mockImplementation(mockWriteText);
mockCopyToClipboard.mockClear();
mockGetResetPasswordToken.mockResolvedValue({
status: 'success',
data: { token: 'reset-tok-abc', id: 'user-1' },
});
});
afterEach(() => {
clipboardSpy?.mockRestore();
});
it('calls getResetPasswordToken and opens the reset link dialog with the generated link', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -421,7 +407,7 @@ describe('EditMemberDrawer', () => {
});
expect(dialog).toHaveTextContent('reset-tok-abc');
fireEvent.click(screen.getByRole('button', { name: /^copy$/i }));
await user.click(screen.getByRole('button', { name: /^copy$/i }));
await waitFor(() => {
expect(mockToast.success).toHaveBeenCalledWith(
@@ -430,7 +416,7 @@ describe('EditMemberDrawer', () => {
);
});
expect(mockWriteText).toHaveBeenCalledWith(
expect(mockCopyToClipboard).toHaveBeenCalledWith(
expect.stringContaining('reset-tok-abc'),
);
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();

View File

@@ -202,19 +202,8 @@ function InviteMembersModal({
onComplete?.();
} catch (err) {
const apiErr = err as APIError;
if (apiErr?.getHttpStatusCode() === 409) {
toast.error(
touchedRows.length === 1
? `${touchedRows[0].email} is already a member`
: 'Invite for one or more users already exists',
{ richColors: true },
);
} else {
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
toast.error(`Failed to send invites: ${errorMessage}`, {
richColors: true,
});
}
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
toast.error(errorMessage, { richColors: true });
} finally {
setIsSubmitting(false);
}

View File

@@ -1,9 +1,18 @@
import { toast } from '@signozhq/sonner';
import inviteUsers from 'api/v1/invite/bulk/create';
import sendInvite from 'api/v1/invite/create';
import { StatusCodes } from 'http-status-codes';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import APIError from 'types/api/error';
import InviteMembersModal from '../InviteMembersModal';
const makeApiError = (message: string, code = StatusCodes.CONFLICT): APIError =>
new APIError({
httpStatusCode: code,
error: { code: 'already_exists', message, url: '', errors: [] },
});
jest.mock('api/v1/invite/create');
jest.mock('api/v1/invite/bulk/create');
jest.mock('@signozhq/sonner', () => ({
@@ -142,6 +151,90 @@ describe('InviteMembersModal', () => {
});
});
describe('error handling', () => {
it('shows BE message on single invite 409', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
mockSendInvite.mockRejectedValue(
makeApiError('An invite already exists for this email: single@signoz.io'),
);
render(<InviteMembersModal {...defaultProps} />);
await user.type(
screen.getAllByPlaceholderText('john@signoz.io')[0],
'single@signoz.io',
);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
'An invite already exists for this email: single@signoz.io',
expect.anything(),
);
});
});
it('shows BE message on bulk invite 409', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
mockInviteUsers.mockRejectedValue(
makeApiError('An invite already exists for this email: alice@signoz.io'),
);
render(<InviteMembersModal {...defaultProps} />);
const emailInputs = screen.getAllByPlaceholderText('john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByText('Viewer'));
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
'An invite already exists for this email: alice@signoz.io',
expect.anything(),
);
});
});
it('shows BE message on generic error', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
mockSendInvite.mockRejectedValue(
makeApiError('Internal server error', StatusCodes.INTERNAL_SERVER_ERROR),
);
render(<InviteMembersModal {...defaultProps} />);
await user.type(
screen.getAllByPlaceholderText('john@signoz.io')[0],
'single@signoz.io',
);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
'Internal server error',
expect.anything(),
);
});
});
});
it('uses inviteUsers (bulk) when multiple rows are filled', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onComplete = jest.fn();

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

@@ -17,6 +17,7 @@ function CodeCopyBtn({
let copiedText = '';
if (children && Array.isArray(children)) {
setIsSnippetCopied(true);
// eslint-disable-next-line no-restricted-properties
navigator.clipboard.writeText(children[0].props.children[0]).finally(() => {
copiedText = (children[0].props.children[0] as string).slice(0, 200); // slicing is done due to the limitation in accepted char length in attributes
setTimeout(() => {

View File

@@ -401,6 +401,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const textToCopy = selectedTexts.join(', ');
// eslint-disable-next-line no-restricted-properties
navigator.clipboard.writeText(textToCopy).catch(console.error);
}, [selectedChips, selectedValues]);

View File

@@ -34,7 +34,7 @@ export function useRoles(): {
export function getRoleOptions(roles: AuthtypesRoleDTO[]): RoleOption[] {
return roles.map((role) => ({
label: role.name ?? '',
value: role.name ?? '',
value: role.id ?? '',
}));
}

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { DialogWrapper } from '@signozhq/dialog';
import { toast } from '@signozhq/sonner';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
@@ -105,19 +106,23 @@ function AddKeyModal(): JSX.Element {
});
}
const [copyState, copyToClipboard] = useCopyToClipboard();
const handleCopy = useCallback(async (): Promise<void> => {
if (!createdKey?.key) {
return;
}
try {
await navigator.clipboard.writeText(createdKey.key);
setHasCopied(true);
setTimeout(() => setHasCopied(false), 2000);
toast.success('Key copied to clipboard', { richColors: true });
} catch {
copyToClipboard(createdKey.key);
setHasCopied(true);
setTimeout(() => setHasCopied(false), 2000);
toast.success('Key copied to clipboard', { richColors: true });
}, [copyToClipboard, createdKey?.key]);
useEffect(() => {
if (copyState.error) {
toast.error('Failed to copy key', { richColors: true });
}
}, [createdKey]);
}, [copyState.error]);
const handleClose = useCallback((): void => {
setIsAddKeyOpen(null);

View File

@@ -1,13 +1,13 @@
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { PowerOff, X } from '@signozhq/icons';
import { Trash2, X } from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getGetServiceAccountQueryKey,
invalidateListServiceAccounts,
useUpdateServiceAccountStatus,
useDeleteServiceAccount,
} from 'api/generated/services/serviceaccount';
import type {
RenderErrorResponseDTO,
@@ -17,14 +17,14 @@ import { AxiosError } from 'axios';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import { parseAsBoolean, useQueryState } from 'nuqs';
function DisableAccountModal(): JSX.Element {
function DeleteAccountModal(): JSX.Element {
const queryClient = useQueryClient();
const [accountId, setAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
const [isDisableOpen, setIsDisableOpen] = useQueryState(
SA_QUERY_PARAMS.DISABLE_SA,
const [isDeleteOpen, setIsDeleteOpen] = useQueryState(
SA_QUERY_PARAMS.DELETE_SA,
parseAsBoolean.withDefault(false),
);
const open = !!isDisableOpen && !!accountId;
const open = !!isDeleteOpen && !!accountId;
const cachedAccount = accountId
? queryClient.getQueryData<{
@@ -34,13 +34,13 @@ function DisableAccountModal(): JSX.Element {
const accountName = cachedAccount?.data?.name;
const {
mutate: updateStatus,
isLoading: isDisabling,
} = useUpdateServiceAccountStatus({
mutate: deleteAccount,
isLoading: isDeleting,
} = useDeleteServiceAccount({
mutation: {
onSuccess: async () => {
toast.success('Service account disabled', { richColors: true });
await setIsDisableOpen(null);
toast.success('Service account deleted', { richColors: true });
await setIsDeleteOpen(null);
await setAccountId(null);
await invalidateListServiceAccounts(queryClient);
},
@@ -48,7 +48,7 @@ function DisableAccountModal(): JSX.Element {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to disable service account';
)?.getErrorMessage() || 'Failed to delete service account';
toast.error(errMessage, { richColors: true });
},
},
@@ -58,14 +58,13 @@ function DisableAccountModal(): JSX.Element {
if (!accountId) {
return;
}
updateStatus({
deleteAccount({
pathParams: { id: accountId },
data: { status: 'DISABLED' },
});
}
function handleCancel(): void {
setIsDisableOpen(null);
setIsDeleteOpen(null);
}
return (
@@ -76,17 +75,18 @@ function DisableAccountModal(): JSX.Element {
handleCancel();
}
}}
title={`Disable service account ${accountName ?? ''}?`}
title={`Delete service account ${accountName ?? ''}?`}
width="narrow"
className="alert-dialog sa-disable-dialog"
className="alert-dialog sa-delete-dialog"
showCloseButton={false}
disableOutsideClick={false}
>
<p className="sa-disable-dialog__body">
Disabling this service account will revoke access for all its keys. Any
systems using this account will lose access immediately.
<p className="sa-delete-dialog__body">
Are you sure you want to delete <strong>{accountName}</strong>? This action
cannot be undone. All keys associated with this service account will be
permanently removed.
</p>
<DialogFooter className="sa-disable-dialog__footer">
<DialogFooter className="sa-delete-dialog__footer">
<Button variant="solid" color="secondary" size="sm" onClick={handleCancel}>
<X size={12} />
Cancel
@@ -95,15 +95,15 @@ function DisableAccountModal(): JSX.Element {
variant="solid"
color="destructive"
size="sm"
loading={isDisabling}
loading={isDeleting}
onClick={handleConfirm}
>
<PowerOff size={12} />
Disable
<Trash2 size={12} />
Delete
</Button>
</DialogFooter>
</DialogWrapper>
);
}
export default DisableAccountModal;
export default DeleteAccountModal;

View File

@@ -6,7 +6,7 @@ import { LockKeyhole, Trash2, X } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { popupContainer } from 'utils/selectPopupContainer';
import { disabledDate, formatLastObservedAt } from '../utils';
@@ -17,7 +17,7 @@ export interface EditKeyFormProps {
register: UseFormRegister<FormValues>;
control: Control<FormValues>;
expiryMode: ExpiryMode;
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null;
keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null;
isSaving: boolean;
isDirty: boolean;
onSubmit: () => void;

View File

@@ -11,7 +11,7 @@ import {
} from 'api/generated/services/serviceaccount';
import type {
RenderErrorResponseDTO,
ServiceaccounttypesFactorAPIKeyDTO,
ServiceaccounttypesGettableFactorAPIKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
@@ -27,7 +27,7 @@ import { DEFAULT_FORM_VALUES, ExpiryMode } from './types';
import './EditKeyModal.styles.scss';
export interface EditKeyModalProps {
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null;
keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null;
}
function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {

View File

@@ -3,7 +3,7 @@ import { Button } from '@signozhq/button';
import { KeyRound, X } from '@signozhq/icons';
import { Skeleton, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs';
@@ -14,7 +14,7 @@ import RevokeKeyModal from './RevokeKeyModal';
import { formatLastObservedAt } from './utils';
interface KeysTabProps {
keys: ServiceaccounttypesFactorAPIKeyDTO[];
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
isLoading: boolean;
isDisabled?: boolean;
currentPage: number;
@@ -44,7 +44,7 @@ function buildColumns({
isDisabled,
onRevokeClick,
handleformatLastObservedAt,
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesFactorAPIKeyDTO> {
}: BuildColumnsParams): ColumnsType<ServiceaccounttypesGettableFactorAPIKeyDTO> {
return [
{
title: 'Name',
@@ -165,7 +165,17 @@ function KeysTab({
return (
<div className="keys-tab__empty">
<KeyRound size={24} className="keys-tab__empty-icon" />
<p className="keys-tab__empty-text">No keys. Start by creating one.</p>
<p className="keys-tab__empty-text">
No keys. Start by creating one.{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/iam/service-accounts/#step-3-generate-an-api-key"
target="_blank"
rel="noopener noreferrer"
className="keys-tab__learn-more"
>
Learn more
</a>
</p>
<Button
type="button"
className="keys-tab__learn-more"
@@ -183,7 +193,7 @@ function KeysTab({
return (
<>
{/* Todo: use new table component from periscope when ready */}
<Table<ServiceaccounttypesFactorAPIKeyDTO>
<Table<ServiceaccounttypesGettableFactorAPIKeyDTO>
columns={columns}
dataSource={keys}
rowKey="id"

View File

@@ -9,6 +9,9 @@ import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import SaveErrorItem from './SaveErrorItem';
import type { SaveError } from './utils';
interface OverviewTabProps {
account: ServiceAccountRow;
localName: string;
@@ -21,6 +24,7 @@ interface OverviewTabProps {
rolesError?: boolean;
rolesErrorObj?: APIError | undefined;
onRefetchRoles?: () => void;
saveErrors?: SaveError[];
}
function OverviewTab({
@@ -35,6 +39,7 @@ function OverviewTab({
rolesError,
rolesErrorObj,
onRefetchRoles,
saveErrors = [],
}: OverviewTabProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
@@ -92,11 +97,14 @@ function OverviewTab({
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<div className="sa-drawer__disabled-roles">
{localRoles.length > 0 ? (
localRoles.map((r) => (
<Badge key={r} color="vanilla">
{r}
</Badge>
))
localRoles.map((roleId) => {
const role = availableRoles.find((r) => r.id === roleId);
return (
<Badge key={roleId} color="vanilla">
{role?.name ?? roleId}
</Badge>
);
})
) : (
<span className="sa-drawer__input-text"></span>
)}
@@ -126,9 +134,13 @@ function OverviewTab({
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
) : account.status?.toUpperCase() === 'DELETED' ? (
<Badge color="cherry" variant="outline">
DELETED
</Badge>
) : (
<Badge color="vanilla" variant="outline" className="sa-status-badge">
DISABLED
{account.status ? account.status.toUpperCase() : 'UNKNOWN'}
</Badge>
)}
</div>
@@ -143,6 +155,19 @@ function OverviewTab({
<Badge color="vanilla">{formatTimestamp(account.updatedAt)}</Badge>
</div>
</div>
{saveErrors.length > 0 && (
<div className="sa-drawer__save-errors">
{saveErrors.map(({ context, apiError, onRetry }) => (
<SaveErrorItem
key={context}
context={context}
apiError={apiError}
onRetry={onRetry}
/>
))}
</div>
)}
</>
);
}

View File

@@ -11,7 +11,7 @@ import {
} from 'api/generated/services/serviceaccount';
import type {
RenderErrorResponseDTO,
ServiceaccounttypesFactorAPIKeyDTO,
ServiceaccounttypesGettableFactorAPIKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
@@ -64,9 +64,9 @@ function RevokeKeyModal(): JSX.Element {
const open = !!revokeKeyId && !!accountId;
const cachedKeys = accountId
? queryClient.getQueryData<{ data: ServiceaccounttypesFactorAPIKeyDTO[] }>(
getListServiceAccountKeysQueryKey({ id: accountId }),
)
? queryClient.getQueryData<{
data: ServiceaccounttypesGettableFactorAPIKeyDTO[];
}>(getListServiceAccountKeysQueryKey({ id: accountId }))
: null;
const keyName = cachedKeys?.data?.find((k) => k.id === revokeKeyId)?.name;

View File

@@ -0,0 +1,74 @@
import { useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { ChevronDown, ChevronUp, CircleAlert, RotateCw } from '@signozhq/icons';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
interface SaveErrorItemProps {
context: string;
apiError: APIError;
onRetry?: () => void | Promise<void>;
}
function SaveErrorItem({
context,
apiError,
onRetry,
}: SaveErrorItemProps): JSX.Element {
const [expanded, setExpanded] = useState(false);
const [isRetrying, setIsRetrying] = useState(false);
const ChevronIcon = expanded ? ChevronUp : ChevronDown;
return (
<div className="sa-error-item">
<div
role="button"
tabIndex={0}
className="sa-error-item__header"
aria-disabled={isRetrying}
onClick={(): void => {
if (!isRetrying) {
setExpanded((prev) => !prev);
}
}}
>
<CircleAlert size={12} className="sa-error-item__icon" />
<span className="sa-error-item__title">
{isRetrying ? 'Retrying...' : `${context}: ${apiError.getErrorMessage()}`}
</span>
{onRetry && !isRetrying && (
<Button
type="button"
aria-label="Retry"
size="xs"
onClick={async (e): Promise<void> => {
e.stopPropagation();
setIsRetrying(true);
setExpanded(false);
try {
await onRetry();
} finally {
setIsRetrying(false);
}
}}
>
<RotateCw size={12} color={Color.BG_CHERRY_400} />
</Button>
)}
{!isRetrying && (
<ChevronIcon size={14} className="sa-error-item__chevron" />
)}
</div>
{expanded && !isRetrying && (
<div className="sa-error-item__body">
<ErrorContent error={apiError} />
</div>
)}
</div>
);
}
export default SaveErrorItem;

View File

@@ -92,6 +92,23 @@
display: flex;
flex-direction: column;
gap: var(--spacing-8);
&::-webkit-scrollbar {
width: 0.25rem;
}
&::-webkit-scrollbar-thumb {
background: rgba(136, 136, 136, 0.4);
border-radius: 0.125rem;
&:hover {
background: rgba(136, 136, 136, 0.7);
}
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
&__footer {
@@ -239,6 +256,113 @@
letter-spacing: 0.48px;
text-transform: uppercase;
}
&__save-errors {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
}
.sa-error-item {
border: 1px solid var(--l1-border);
border-radius: 4px;
overflow: hidden;
&__header {
display: flex;
align-items: center;
gap: var(--spacing-3);
width: 100%;
padding: var(--padding-2) var(--padding-4);
background: transparent;
border: none;
cursor: pointer;
text-align: left;
outline: none;
&:hover {
background: rgba(229, 72, 77, 0.08);
}
&:focus-visible {
outline: 2px solid var(--primary);
outline-offset: -2px;
}
&[aria-disabled='true'] {
cursor: default;
pointer-events: none;
}
}
&:hover {
border-color: var(--callout-error-border);
}
&__icon {
flex-shrink: 0;
color: var(--bg-cherry-500);
}
&__title {
flex: 1;
min-width: 0;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--bg-cherry-500);
line-height: var(--line-height-18);
letter-spacing: -0.06px;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__chevron {
flex-shrink: 0;
color: var(--l2-foreground);
}
&__body {
border-top: 1px solid var(--l1-border);
.error-content {
&__summary {
padding: 10px 12px;
}
&__summary-left {
gap: 6px;
}
&__error-code {
font-size: 12px;
line-height: 18px;
}
&__error-message {
font-size: 11px;
line-height: 16px;
}
&__docs-button {
font-size: 11px;
padding: 5px 8px;
}
&__message-badge {
padding: 0 12px 10px;
gap: 8px;
}
&__message-item {
font-size: 11px;
padding: 2px 12px 2px 22px;
margin-bottom: 2px;
}
}
}
}
.keys-tab {
@@ -429,7 +553,7 @@
}
}
.sa-disable-dialog {
.sa-delete-dialog {
background: var(--l2-background);
border: 1px solid var(--l2-border);

View File

@@ -1,25 +1,29 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/button';
import { DrawerWrapper } from '@signozhq/drawer';
import { Key, LayoutGrid, Plus, PowerOff, X } from '@signozhq/icons';
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getListServiceAccountsQueryKey,
useGetServiceAccount,
useListServiceAccountKeys,
useUpdateServiceAccount,
} from 'api/generated/services/serviceaccount';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import {
ServiceAccountRow,
ServiceAccountStatus,
toServiceAccountRow,
} from 'container/ServiceAccountsSettings/utils';
import { useServiceAccountRoleManager } from 'hooks/serviceAccount/useServiceAccountRoleManager';
import {
parseAsBoolean,
parseAsInteger,
@@ -27,12 +31,14 @@ import {
parseAsStringEnum,
useQueryState,
} from 'nuqs';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import AddKeyModal from './AddKeyModal';
import DisableAccountModal from './DisableAccountModal';
import DeleteAccountModal from './DeleteAccountModal';
import KeysTab from './KeysTab';
import OverviewTab from './OverviewTab';
import type { SaveError } from './utils';
import { ServiceAccountDrawerTab } from './utils';
import './ServiceAccountDrawer.styles.scss';
@@ -69,12 +75,16 @@ function ServiceAccountDrawer({
SA_QUERY_PARAMS.ADD_KEY,
parseAsBoolean.withDefault(false),
);
const [, setIsDisableOpen] = useQueryState(
SA_QUERY_PARAMS.DISABLE_SA,
const [, setIsDeleteOpen] = useQueryState(
SA_QUERY_PARAMS.DELETE_SA,
parseAsBoolean.withDefault(false),
);
const [localName, setLocalName] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
const queryClient = useQueryClient();
const {
data: accountData,
@@ -93,21 +103,30 @@ function ServiceAccountDrawer({
[accountData],
);
const { currentRoles, applyDiff } = useServiceAccountRoleManager(
selectedAccountId ?? '',
);
useEffect(() => {
if (account) {
setLocalName(account.name ?? '');
setLocalRoles(account.roles ?? []);
if (account?.id) {
setLocalName(account?.name ?? '');
setKeysPage(1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [account?.id]);
setSaveErrors([]);
}, [account?.id, account?.name, setKeysPage]);
const isDisabled = account?.status?.toUpperCase() !== 'ACTIVE';
useEffect(() => {
setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]);
}, [currentRoles]);
const isDeleted =
account?.status?.toUpperCase() === ServiceAccountStatus.Deleted;
const isDirty =
account !== null &&
(localName !== (account.name ?? '') ||
JSON.stringify(localRoles) !== JSON.stringify(account.roles ?? []));
JSON.stringify([...localRoles].sort()) !==
JSON.stringify([...currentRoles.map((r) => r.id).filter(Boolean)].sort()));
const {
roles: availableRoles,
@@ -133,51 +152,189 @@ function ServiceAccountDrawer({
}
}, [keysLoading, keys.length, keysPage, setKeysPage]);
const { mutate: updateAccount, isLoading: isSaving } = useUpdateServiceAccount(
{
mutation: {
onSuccess: () => {
toast.success('Service account updated successfully', {
richColors: true,
});
refetchAccount();
onSuccess({ closeDrawer: false });
},
onError: (error) => {
const errMessage =
convertToApiError(
error as AxiosError<RenderErrorResponseDTO, unknown> | null,
)?.getErrorMessage() || 'Failed to update service account';
toast.error(errMessage, { richColors: true });
},
},
},
// the retry for this mutation is safe due to the api being idempotent on backend
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
const toSaveApiError = useCallback(
(err: unknown): APIError =>
convertToApiError(err as AxiosError<RenderErrorResponseDTO>) ??
toAPIError(err as AxiosError<RenderErrorResponseDTO>),
[],
);
function handleSave(): void {
const retryNameUpdate = useCallback(async (): Promise<void> => {
if (!account) {
return;
}
try {
await updateMutateAsync({
pathParams: { id: account.id },
data: { name: localName },
});
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
refetchAccount();
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
} catch (err) {
setSaveErrors((prev) =>
prev.map((e) =>
e.context === 'Name update' ? { ...e, apiError: toSaveApiError(err) } : e,
),
);
}
}, [
account,
localName,
updateMutateAsync,
refetchAccount,
queryClient,
toSaveApiError,
]);
const handleNameChange = useCallback((name: string): void => {
setLocalName(name);
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
}, []);
const makeRoleRetry = useCallback(
(
context: string,
rawRetry: () => Promise<void>,
) => async (): Promise<void> => {
try {
await rawRetry();
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
} catch (err) {
setSaveErrors((prev) =>
prev.map((e) =>
e.context === context ? { ...e, apiError: toSaveApiError(err) } : e,
),
);
}
},
[toSaveApiError],
);
const retryRolesUpdate = useCallback(async (): Promise<void> => {
try {
const failures = await applyDiff(localRoles, availableRoles);
if (failures.length === 0) {
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update'));
} else {
setSaveErrors((prev) => {
const rest = prev.filter((e) => e.context !== 'Roles update');
const roleErrors = failures.map((f) => {
const ctx = `Role '${f.roleName}'`;
return {
context: ctx,
apiError: toSaveApiError(f.error),
onRetry: makeRoleRetry(ctx, f.onRetry),
};
});
return [...rest, ...roleErrors];
});
}
} catch (err) {
setSaveErrors((prev) =>
prev.map((e) =>
e.context === 'Roles update' ? { ...e, apiError: toSaveApiError(err) } : e,
),
);
}
}, [localRoles, availableRoles, applyDiff, toSaveApiError, makeRoleRetry]);
const handleSave = useCallback(async (): Promise<void> => {
if (!account || !isDirty) {
return;
}
updateAccount({
pathParams: { id: account.id },
data: { name: localName, email: account.email, roles: localRoles },
});
}
setSaveErrors([]);
setIsSaving(true);
try {
const namePromise =
localName !== (account.name ?? '')
? updateMutateAsync({
pathParams: { id: account.id },
data: { name: localName },
})
: Promise.resolve();
const [nameResult, rolesResult] = await Promise.allSettled([
namePromise,
applyDiff(localRoles, availableRoles),
]);
const errors: SaveError[] = [];
if (nameResult.status === 'rejected') {
errors.push({
context: 'Name update',
apiError: toSaveApiError(nameResult.reason),
onRetry: retryNameUpdate,
});
}
if (rolesResult.status === 'rejected') {
errors.push({
context: 'Roles update',
apiError: toSaveApiError(rolesResult.reason),
onRetry: retryRolesUpdate,
});
} else {
for (const failure of rolesResult.value) {
const context = `Role '${failure.roleName}'`;
errors.push({
context,
apiError: toSaveApiError(failure.error),
onRetry: makeRoleRetry(context, failure.onRetry),
});
}
}
if (errors.length > 0) {
setSaveErrors(errors);
} else {
toast.success('Service account updated successfully', {
richColors: true,
});
onSuccess({ closeDrawer: false });
}
refetchAccount();
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
} finally {
setIsSaving(false);
}
}, [
account,
isDirty,
localName,
localRoles,
availableRoles,
updateMutateAsync,
applyDiff,
refetchAccount,
onSuccess,
queryClient,
toSaveApiError,
retryNameUpdate,
makeRoleRetry,
retryRolesUpdate,
]);
const handleClose = useCallback((): void => {
setIsDisableOpen(null);
setIsDeleteOpen(null);
setIsAddKeyOpen(null);
setSelectedAccountId(null);
setActiveTab(null);
setKeysPage(null);
setEditKeyId(null);
setSaveErrors([]);
}, [
setSelectedAccountId,
setActiveTab,
setKeysPage,
setEditKeyId,
setIsAddKeyOpen,
setIsDisableOpen,
setIsDeleteOpen,
]);
const drawerContent = (
@@ -220,7 +377,7 @@ function ServiceAccountDrawer({
variant="outlined"
size="sm"
color="secondary"
disabled={isDisabled}
disabled={isDeleted}
onClick={(): void => {
setIsAddKeyOpen(true);
}}
@@ -251,22 +408,23 @@ function ServiceAccountDrawer({
<OverviewTab
account={account}
localName={localName}
onNameChange={setLocalName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={setLocalRoles}
isDisabled={isDisabled}
isDisabled={isDeleted}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
)}
{activeTab === ServiceAccountDrawerTab.Keys && (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDisabled}
isDisabled={isDeleted}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
@@ -298,20 +456,20 @@ function ServiceAccountDrawer({
/>
) : (
<>
{!isDisabled && (
{!isDeleted && (
<Button
variant="ghost"
color="destructive"
className="sa-drawer__footer-btn"
onClick={(): void => {
setIsDisableOpen(true);
setIsDeleteOpen(true);
}}
>
<PowerOff size={12} />
Disable Service Account
<Trash2 size={12} />
Delete Service Account
</Button>
)}
{!isDisabled && (
{!isDeleted && (
<div className="sa-drawer__footer-right">
<Button
variant="solid"
@@ -359,7 +517,7 @@ function ServiceAccountDrawer({
className="sa-drawer"
/>
<DisableAccountModal />
<DeleteAccountModal />
<AddKeyModal />
</>

View File

@@ -9,6 +9,16 @@ jest.mock('@signozhq/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
const mockCopyToClipboard = jest.fn();
const mockCopyState = { value: undefined, error: undefined };
jest.mock('react-use', () => ({
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
mockCopyState,
mockCopyToClipboard,
],
}));
const mockToast = jest.mocked(toast);
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys';
@@ -35,16 +45,9 @@ function renderModal(): ReturnType<typeof render> {
}
describe('AddKeyModal', () => {
beforeAll(() => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: jest.fn().mockResolvedValue(undefined) },
configurable: true,
writable: true,
});
});
beforeEach(() => {
jest.clearAllMocks();
mockCopyToClipboard.mockClear();
server.use(
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json(createdKeyResponse)),
@@ -90,9 +93,6 @@ describe('AddKeyModal', () => {
it('copy button writes key to clipboard and shows toast.success', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const writeTextSpy = jest
.spyOn(navigator.clipboard, 'writeText')
.mockResolvedValue(undefined);
renderModal();
@@ -115,14 +115,12 @@ describe('AddKeyModal', () => {
await user.click(copyBtn);
await waitFor(() => {
expect(writeTextSpy).toHaveBeenCalledWith('snz_abc123xyz456secret');
expect(mockCopyToClipboard).toHaveBeenCalledWith('snz_abc123xyz456secret');
expect(mockToast.success).toHaveBeenCalledWith(
'Key copied to clipboard',
expect.anything(),
);
});
writeTextSpy.mockRestore();
});
it('Cancel button closes the modal', async () => {

View File

@@ -1,5 +1,5 @@
import { toast } from '@signozhq/sonner';
import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -14,17 +14,16 @@ const mockToast = jest.mocked(toast);
const SA_KEY_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys/key-1';
const mockKey: ServiceaccounttypesFactorAPIKeyDTO = {
const mockKey: ServiceaccounttypesGettableFactorAPIKeyDTO = {
id: 'key-1',
name: 'Original Key Name',
expiresAt: 0,
lastObservedAt: null as any,
key: 'snz_abc123',
serviceAccountId: 'sa-1',
};
function renderModal(
keyItem: ServiceaccounttypesFactorAPIKeyDTO | null = mockKey,
keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null = mockKey,
searchParams: Record<string, string> = {
account: 'sa-1',
'edit-key': 'key-1',

View File

@@ -1,5 +1,5 @@
import { toast } from '@signozhq/sonner';
import { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -14,13 +14,12 @@ const mockToast = jest.mocked(toast);
const SA_KEY_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys/:fid';
const keys: ServiceaccounttypesFactorAPIKeyDTO[] = [
const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
{
id: 'key-1',
name: 'Production Key',
expiresAt: 0,
lastObservedAt: null as any,
key: 'snz_prod_123',
serviceAccountId: 'sa-1',
},
{
@@ -28,7 +27,6 @@ const keys: ServiceaccounttypesFactorAPIKeyDTO[] = [
name: 'Staging Key',
expiresAt: 1924905600, // 2030-12-31
lastObservedAt: new Date('2026-03-10T10:00:00Z'),
key: 'snz_stag_456',
serviceAccountId: 'sa-1',
},
];

View File

@@ -23,7 +23,9 @@ jest.mock('@signozhq/sonner', () => ({
const ROLES_ENDPOINT = '*/api/v1/roles';
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';
const SA_STATUS_ENDPOINT = '*/api/v1/service_accounts/sa-1/status';
const SA_DELETE_ENDPOINT = '*/api/v1/service_accounts/sa-1';
const SA_ROLES_ENDPOINT = '*/api/v1/service_accounts/:id/roles';
const SA_ROLE_DELETE_ENDPOINT = '*/api/v1/service_accounts/:id/roles/:rid';
const activeAccountResponse = {
id: 'sa-1',
@@ -35,10 +37,10 @@ const activeAccountResponse = {
updatedAt: '2026-01-02T00:00:00Z',
};
const disabledAccountResponse = {
const deletedAccountResponse = {
...activeAccountResponse,
id: 'sa-2',
status: 'DISABLED',
status: 'DELETED',
};
function renderDrawer(
@@ -67,7 +69,23 @@ describe('ServiceAccountDrawer', () => {
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.put(SA_STATUS_ENDPOINT, (_, res, ctx) =>
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: listRolesSuccessResponse.data.filter(
(r) => r.name === 'signoz-admin',
),
}),
),
),
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
);
@@ -115,8 +133,6 @@ describe('ServiceAccountDrawer', () => {
expect(updateSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: 'CI Bot Updated',
email: 'ci-bot@signoz.io',
roles: ['signoz-admin'],
}),
);
expect(onSuccess).toHaveBeenCalledWith({ closeDrawer: false });
@@ -125,6 +141,7 @@ describe('ServiceAccountDrawer', () => {
it('changing roles enables Save; clicking Save sends updated roles in payload', async () => {
const updateSpy = jest.fn();
const roleSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
@@ -132,6 +149,10 @@ describe('ServiceAccountDrawer', () => {
updateSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
rest.post(SA_ROLES_ENDPOINT, async (req, res, ctx) => {
roleSpy(await req.json());
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
renderDrawer();
@@ -146,21 +167,22 @@ describe('ServiceAccountDrawer', () => {
await user.click(saveBtn);
await waitFor(() => {
expect(updateSpy).toHaveBeenCalledWith(
expect(updateSpy).not.toHaveBeenCalled();
expect(roleSpy).toHaveBeenCalledWith(
expect.objectContaining({
roles: expect.arrayContaining(['signoz-admin', 'signoz-viewer']),
id: '019c24aa-2248-7585-a129-4188b3473c27',
}),
);
});
});
it('"Disable Service Account" opens confirm dialog; confirming sends correct status payload', async () => {
const statusSpy = jest.fn();
it('"Delete Service Account" opens confirm dialog; confirming sends delete request', async () => {
const deleteSpy = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.put(SA_STATUS_ENDPOINT, async (req, res, ctx) => {
statusSpy(await req.json());
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) => {
deleteSpy();
return res(ctx.status(200), ctx.json({ status: 'success', data: {} }));
}),
);
@@ -170,19 +192,19 @@ describe('ServiceAccountDrawer', () => {
await screen.findByDisplayValue('CI Bot');
await user.click(
screen.getByRole('button', { name: /Disable Service Account/i }),
screen.getByRole('button', { name: /Delete Service Account/i }),
);
const dialog = await screen.findByRole('dialog', {
name: /Disable service account CI Bot/i,
name: /Delete service account CI Bot/i,
});
expect(dialog).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /^Disable$/i });
const confirmBtns = screen.getAllByRole('button', { name: /^Delete$/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await waitFor(() => {
expect(statusSpy).toHaveBeenCalledWith({ status: 'DISABLED' });
expect(deleteSpy).toHaveBeenCalled();
});
await waitFor(() => {
@@ -190,14 +212,17 @@ describe('ServiceAccountDrawer', () => {
});
});
it('disabled account shows read-only name, no Save button, no Disable button', async () => {
it('deleted account shows read-only name, no Save button, no Delete button', async () => {
server.use(
rest.get('*/api/v1/service_accounts/sa-2', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: disabledAccountResponse })),
res(ctx.status(200), ctx.json({ data: deletedAccountResponse })),
),
rest.get('*/api/v1/service_accounts/sa-2/keys', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
rest.get('*/api/v1/service_accounts/sa-2/roles', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
);
renderDrawer({ account: 'sa-2' });
@@ -208,7 +233,7 @@ describe('ServiceAccountDrawer', () => {
screen.queryByRole('button', { name: /Save Changes/i }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: /Disable Service Account/i }),
screen.queryByRole('button', { name: /Delete Service Account/i }),
).not.toBeInTheDocument();
expect(screen.queryByDisplayValue('CI Bot')).not.toBeInTheDocument();
});
@@ -248,3 +273,169 @@ describe('ServiceAccountDrawer', () => {
).toBeInTheDocument();
});
});
describe('ServiceAccountDrawer save-error UX', () => {
beforeEach(() => {
jest.clearAllMocks();
server.use(
rest.get(ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(listRolesSuccessResponse)),
),
rest.get(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: [] })),
),
rest.get(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ data: activeAccountResponse })),
),
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.delete(SA_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.get(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: listRolesSuccessResponse.data.filter(
(r) => r.name === 'signoz-admin',
),
}),
),
),
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
rest.delete(SA_ROLE_DELETE_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
);
});
afterEach(() => {
server.resetHandlers();
});
it('name update failure shows SaveErrorItem with "Name update" context', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(500),
ctx.json({
error: {
code: 'INTERNAL_ERROR',
message: 'name update failed',
},
}),
),
),
);
renderDrawer();
const nameInput = await screen.findByDisplayValue('CI Bot');
await user.clear(nameInput);
await user.type(nameInput, 'New Name');
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
expect(
await screen.findByText(/Name update.*name update failed/i, undefined, {
timeout: 5000,
}),
).toBeInTheDocument();
});
it('role update failure shows SaveErrorItem with the role name context', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.post(SA_ROLES_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(500),
ctx.json({
error: {
code: 'INTERNAL_ERROR',
message: 'role assign failed',
},
}),
),
),
);
renderDrawer();
await screen.findByDisplayValue('CI Bot');
// Add the signoz-viewer role (which is not currently assigned)
await user.click(screen.getByLabelText('Roles'));
await user.click(await screen.findByTitle('signoz-viewer'));
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
expect(
await screen.findByText(
/Role 'signoz-viewer'.*role assign failed/i,
undefined,
{
timeout: 5000,
},
),
).toBeInTheDocument();
});
it('clicking Retry on a name-update error re-triggers the request; on success the error item is removed', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// First: PUT always fails so the error appears
server.use(
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(500),
ctx.json({
error: {
code: 'INTERNAL_ERROR',
message: 'name update failed',
},
}),
),
),
);
renderDrawer();
const nameInput = await screen.findByDisplayValue('CI Bot');
await user.clear(nameInput);
await user.type(nameInput, 'Retry Test');
const saveBtn = screen.getByRole('button', { name: /Save Changes/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
await screen.findByText(/Name update.*name update failed/i, undefined, {
timeout: 5000,
});
server.use(
rest.put(SA_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
);
const retryBtn = screen.getByRole('button', { name: /Retry/i });
await user.click(retryBtn);
// Error item should be removed after successful retry
await waitFor(() => {
expect(
screen.queryByText(/Name update.*name update failed/i),
).not.toBeInTheDocument();
});
});
});

View File

@@ -1,6 +1,13 @@
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import APIError from 'types/api/error';
export interface SaveError {
context: string;
apiError: APIError;
onRetry: () => Promise<void>;
}
export enum ServiceAccountDrawerTab {
Overview = 'overview',

View File

@@ -8,7 +8,6 @@ const mockActiveAccount: ServiceAccountRow = {
id: 'sa-1',
name: 'CI Bot',
email: 'ci-bot@signoz.io',
roles: ['signoz-admin'],
status: 'ACTIVE',
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-02T00:00:00Z',
@@ -18,7 +17,6 @@ const mockDisabledAccount: ServiceAccountRow = {
id: 'sa-2',
name: 'Legacy Bot',
email: 'legacy@signoz.io',
roles: ['signoz-viewer', 'signoz-editor', 'billing-manager'],
status: 'DISABLED',
createdAt: '2025-06-01T00:00:00Z',
updatedAt: '2025-12-01T00:00:00Z',
@@ -39,7 +37,6 @@ describe('ServiceAccountsTable', () => {
expect(screen.getByText('CI Bot')).toBeInTheDocument();
expect(screen.getByText('ci-bot@signoz.io')).toBeInTheDocument();
expect(screen.getByText('signoz-admin')).toBeInTheDocument();
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
});
@@ -49,8 +46,6 @@ describe('ServiceAccountsTable', () => {
);
expect(screen.getByText('DISABLED')).toBeInTheDocument();
expect(screen.getByText('signoz-viewer')).toBeInTheDocument();
expect(screen.getByText('+2')).toBeInTheDocument();
});
it('calls onRowClick with the correct account when a row is clicked', async () => {

View File

@@ -25,32 +25,6 @@ export function NameEmailCell({
);
}
export function RolesCell({ roles }: { roles: string[] }): JSX.Element {
if (!roles || roles.length === 0) {
return <span className="sa-dash"></span>;
}
const first = roles[0];
const overflow = roles.length - 1;
const tooltipContent = roles.slice(1).join(', ');
return (
<div className="sa-roles-cell">
<Badge color="vanilla">{first}</Badge>
{overflow > 0 && (
<Tooltip
title={tooltipContent}
overlayClassName="sa-tooltip"
overlayStyle={{ maxWidth: '600px' }}
>
<Badge color="vanilla" variant="outline" className="sa-status-badge">
+{overflow}
</Badge>
</Tooltip>
)}
</div>
);
}
export function StatusBadge({ status }: { status: string }): JSX.Element {
if (status?.toUpperCase() === 'ACTIVE') {
return (
@@ -59,9 +33,16 @@ export function StatusBadge({ status }: { status: string }): JSX.Element {
</Badge>
);
}
if (status?.toUpperCase() === 'DELETED') {
return (
<Badge color="cherry" variant="outline">
DELETED
</Badge>
);
}
return (
<Badge color="vanilla" variant="outline" className="sa-status-badge">
DISABLED
{status ? status.toUpperCase() : 'UNKNOWN'}
</Badge>
);
}
@@ -98,13 +79,6 @@ export const columns: ColumnsType<ServiceAccountRow> = [
<NameEmailCell name={record.name} email={record.email} />
),
},
{
title: 'Roles',
dataIndex: 'roles',
key: 'roles',
width: 420,
render: (roles: string[]): JSX.Element => <RolesCell roles={roles} />,
},
{
title: 'Status',
dataIndex: 'status',

View File

@@ -38,7 +38,6 @@ const ROUTES = {
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong',
UN_AUTHORIZED: '/un-authorized',

View File

@@ -249,14 +249,34 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
perform: (): void => navigate(ROUTES.BILLING),
},
{
id: 'my-settings-api-keys',
name: 'Go to Account Settings API Keys',
shortcut: [GlobalShortcutsName.NavigateToSettingsAPIKeys],
keywords: 'account settings api keys',
id: 'my-settings-service-accounts',
name: 'Go to Service Accounts',
shortcut: [GlobalShortcutsName.NavigateToSettingsServiceAccounts],
keywords: 'settings service accounts',
section: 'Settings',
icon: <Settings size={14} />,
roles: ['ADMIN', 'EDITOR'],
perform: (): void => navigate(ROUTES.API_KEYS),
roles: ['ADMIN'],
perform: (): void => navigate(ROUTES.SERVICE_ACCOUNTS_SETTINGS),
},
{
id: 'my-settings-roles',
name: 'Go to Roles',
shortcut: [GlobalShortcutsName.NavigateToSettingsRoles],
keywords: 'settings roles',
section: 'Settings',
icon: <Settings size={14} />,
roles: ['ADMIN'],
perform: (): void => navigate(ROUTES.ROLES_SETTINGS),
},
{
id: 'my-settings-members',
name: 'Go to Members',
shortcut: [GlobalShortcutsName.NavigateToSettingsMembers],
keywords: 'settings members',
section: 'Settings',
icon: <Settings size={14} />,
roles: ['ADMIN'],
perform: (): void => navigate(ROUTES.MEMBERS_SETTINGS),
},
];
}

View File

@@ -26,8 +26,10 @@ export const GlobalShortcuts = {
NavigateToSettings: 'shift+g',
NavigateToSettingsIngestion: 'shift+g+i',
NavigateToSettingsBilling: 'shift+g+b',
NavigateToSettingsAPIKeys: 'shift+g+k',
NavigateToSettingsNotificationChannels: 'shift+g+n',
NavigateToSettingsServiceAccounts: 'shift+g+k',
NavigateToSettingsRoles: 'shift+g+r',
NavigateToSettingsMembers: 'shift+g+m',
};
export const GlobalShortcutsName = {
@@ -47,8 +49,10 @@ export const GlobalShortcutsName = {
NavigateToSettings: 'shift+g',
NavigateToSettingsIngestion: 'shift+g+i',
NavigateToSettingsBilling: 'shift+g+b',
NavigateToSettingsAPIKeys: 'shift+g+k',
NavigateToSettingsNotificationChannels: 'shift+g+n',
NavigateToSettingsServiceAccounts: 'shift+g+k',
NavigateToSettingsRoles: 'shift+g+r',
NavigateToSettingsMembers: 'shift+g+m',
NavigateToLogs: 'shift+l',
NavigateToLogsPipelines: 'shift+l+p',
NavigateToLogsViews: 'shift+l+v',
@@ -72,9 +76,11 @@ export const GlobalShortcutsDescription = {
NavigateToSettings: 'Navigate to Settings',
NavigateToSettingsIngestion: 'Navigate to Ingestion Settings',
NavigateToSettingsBilling: 'Navigate to Billing Settings',
NavigateToSettingsAPIKeys: 'Navigate to API Keys Settings',
NavigateToSettingsNotificationChannels:
'Navigate to Notification Channels Settings',
NavigateToLogsPipelines: 'Navigate to Logs Pipelines',
NavigateToLogsViews: 'Navigate to Logs Views',
NavigateToSettingsServiceAccounts: 'Navigate to Service Accounts Settings',
NavigateToSettingsRoles: 'Navigate to Roles Settings',
NavigateToSettingsMembers: 'Navigate to Members Settings',
};

View File

@@ -1,685 +0,0 @@
.api-key-container {
margin-top: 24px;
display: flex;
justify-content: center;
width: 100%;
.api-key-content {
width: calc(100% - 30px);
max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.api-keys-search-add-new {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 0;
.add-new-api-key-btn {
display: flex;
align-items: center;
gap: 8px;
}
}
.ant-table-row {
.ant-table-cell {
padding: 0;
border: none;
background: var(--bg-ink-500);
}
.column-render {
margin: 8px 0 !important;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.title-with-action {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
.api-key-data {
display: flex;
gap: 8px;
align-items: center;
.api-key-title {
display: flex;
align-items: center;
gap: 6px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
}
}
.api-key-value {
display: flex;
align-items: center;
gap: 12px;
border-radius: 20px;
padding: 0px 12px;
background: var(--bg-ink-200);
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-xs);
font-family: 'Space Mono', monospace;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
}
.copy-key-btn {
cursor: pointer;
}
}
}
.action-btn {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.visibility-btn {
border: 1px solid rgba(113, 144, 249, 0.2);
background: rgba(113, 144, 249, 0.1);
}
}
.ant-collapse {
border: none;
.ant-collapse-header {
padding: 0px 8px;
display: flex;
align-items: center;
background-color: #121317;
}
.ant-collapse-content {
border-top: 1px solid var(--bg-slate-500);
}
.ant-collapse-item {
border-bottom: none;
}
.ant-collapse-expand-icon {
padding-inline-end: 0px;
}
}
.api-key-details {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border-top: 1px solid var(--bg-slate-500);
padding: 8px;
.api-key-tag {
width: 14px;
height: 14px;
border-radius: 50px;
background: var(--bg-slate-300);
display: flex;
justify-content: center;
align-items: center;
.tag-text {
color: var(--bg-vanilla-400);
leading-trim: both;
text-edge: cap;
font-size: 10px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: -0.05px;
}
}
.api-key-created-by {
margin-left: 8px;
}
.api-key-last-used-at {
display: flex;
align-items: center;
gap: 8px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
}
}
.api-key-expires-in {
font-style: normal;
font-weight: 400;
line-height: 18px;
display: flex;
align-items: center;
gap: 8px;
.dot {
height: 6px;
width: 6px;
border-radius: 50%;
}
&.warning {
color: var(--bg-amber-400);
.dot {
background: var(--bg-amber-400);
box-shadow: 0px 0px 6px 0px var(--bg-amber-400);
}
}
&.danger {
color: var(--bg-cherry-400);
.dot {
background: var(--bg-cherry-400);
box-shadow: 0px 0px 6px 0px var(--bg-cherry-400);
}
}
}
}
}
}
.ant-pagination-item {
display: flex;
justify-content: center;
align-items: center;
> a {
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
}
}
.ant-pagination-item-active {
background-color: var(--bg-robin-500);
> a {
color: var(--bg-ink-500) !important;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
}
}
}
}
.api-key-info-container {
display: flex;
gap: 12px;
flex-direction: column;
.user-info {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
.user-avatar {
background-color: lightslategray;
vertical-align: middle;
}
}
.user-email {
display: inline-flex;
align-items: center;
gap: 12px;
border-radius: 20px;
padding: 0px 12px;
background: var(--bg-ink-200);
font-family: 'Space Mono', monospace;
}
.role {
display: flex;
align-items: center;
gap: 12px;
}
}
.api-key-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--bg-slate-500);
padding: 16px;
}
.ant-modal-close-x {
font-size: 12px;
}
.ant-modal-body {
padding: 12px 16px;
}
.ant-modal-footer {
padding: 16px;
margin-top: 0;
display: flex;
justify-content: flex-end;
}
}
}
.api-key-access-role {
display: flex;
.ant-radio-button-wrapper {
font-size: 12px;
text-transform: capitalize;
&.ant-radio-button-wrapper-checked {
color: #fff;
background: var(--bg-slate-400, #1d212d);
border-color: var(--bg-slate-400, #1d212d);
&:hover {
color: #fff;
background: var(--bg-slate-400, #1d212d);
border-color: var(--bg-slate-400, #1d212d);
&::before {
background-color: var(--bg-slate-400, #1d212d);
}
}
&:focus {
color: #fff;
background: var(--bg-slate-400, #1d212d);
border-color: var(--bg-slate-400, #1d212d);
}
}
}
.tab {
border: 1px solid var(--bg-slate-400);
flex: 1;
display: flex;
justify-content: center;
&::before {
background: var(--bg-slate-400);
}
&.selected {
background: var(--bg-slate-400, #1d212d);
}
}
.role {
display: flex;
align-items: center;
gap: 8px;
}
}
.delete-api-key-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
}
.ant-modal-body {
padding: 0px 16px 28px 16px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.api-key-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
.cancel-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-slate-500);
}
.delete-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-cherry-500);
margin-left: 12px;
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
}
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
}
}
.expiration-selector {
.ant-select-selector {
border: 1px solid var(--bg-slate-400) !important;
}
}
.newAPIKeyDetails {
display: flex;
flex-direction: column;
gap: 8px;
}
.copyable-text {
display: inline-flex;
align-items: center;
gap: 12px;
border-radius: 20px;
padding: 0px 12px;
background: var(--bg-ink-200, #23262e);
.copy-key-btn {
cursor: pointer;
}
}
.lightMode {
.api-key-container {
.api-key-content {
.title {
color: var(--bg-ink-500);
}
.ant-table-row {
.ant-table-cell {
background: var(--bg-vanilla-200);
}
&:hover {
.ant-table-cell {
background: var(--bg-vanilla-200) !important;
}
}
.column-render {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-collapse {
border: none;
.ant-collapse-header {
background: var(--bg-vanilla-100);
}
.ant-collapse-content {
border-top: 1px solid var(--bg-vanilla-300);
}
}
.title-with-action {
.api-key-title {
.ant-typography {
color: var(--bg-ink-500);
}
}
.api-key-value {
background: var(--bg-vanilla-200);
.ant-typography {
color: var(--bg-slate-400);
}
.copy-key-btn {
cursor: pointer;
}
}
.action-btn {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
.api-key-details {
border-top: 1px solid var(--bg-vanilla-200);
.api-key-tag {
background: var(--bg-vanilla-200);
.tag-text {
color: var(--bg-ink-500);
}
}
.api-key-created-by {
color: var(--bg-ink-500);
}
.api-key-last-used-at {
.ant-typography {
color: var(--bg-ink-500);
}
}
}
}
}
}
}
.delete-api-key-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
.title {
color: var(--bg-ink-500);
}
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-500);
}
.api-key-input {
.ant-input {
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--bg-vanilla-300);
color: var(--bg-ink-400);
}
}
}
}
.api-key-info-container {
.user-email {
background: var(--bg-vanilla-200);
}
}
.api-key-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--bg-vanilla-200);
padding: 16px;
}
}
}
.api-key-access-role {
.ant-radio-button-wrapper {
&.ant-radio-button-wrapper-checked {
color: var(--bg-ink-400);
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
&:hover {
color: var(--bg-ink-400);
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
&::before {
background-color: var(--bg-vanilla-300);
}
}
&:focus {
color: var(--bg-ink-400);
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
}
}
}
.tab {
border: 1px solid var(--bg-vanilla-300);
&::before {
background: var(--bg-vanilla-300);
}
&.selected {
background: var(--bg-vanilla-300);
}
}
}
.copyable-text {
background: var(--bg-vanilla-200);
}
}

View File

@@ -1,99 +0,0 @@
import {
createAPIKeyResponse,
getAPIKeysResponse,
} from 'mocks-server/__mockdata__/apiKeys';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
import APIKeys from './APIKeys';
const apiKeysURL = 'http://localhost/api/v1/pats';
describe('APIKeys component', () => {
beforeEach(() => {
server.use(
rest.get(apiKeysURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(getAPIKeysResponse)),
),
);
render(<APIKeys />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders APIKeys component without crashing', () => {
expect(screen.getByText('API Keys')).toBeInTheDocument();
expect(
screen.getByText('Create and manage API keys for the SigNoz API'),
).toBeInTheDocument();
});
it('render list of Access Tokens', async () => {
server.use(
rest.get(apiKeysURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(getAPIKeysResponse)),
),
);
await waitFor(() => {
expect(screen.getByText('No Expiry Key')).toBeInTheDocument();
expect(screen.getByText('1-5 of 18 keys')).toBeInTheDocument();
});
});
it('opens add new key modal on button click', async () => {
fireEvent.click(screen.getByText('New Key'));
await waitFor(() => {
const createNewKeyBtn = screen.getByRole('button', {
name: /Create new key/i,
});
expect(createNewKeyBtn).toBeInTheDocument();
});
});
it('closes add new key modal on cancel button click', async () => {
fireEvent.click(screen.getByText('New Key'));
const createNewKeyBtn = screen.getByRole('button', {
name: /Create new key/i,
});
await waitFor(() => {
expect(createNewKeyBtn).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Cancel'));
await waitFor(() => {
expect(createNewKeyBtn).not.toBeInTheDocument();
});
});
it('creates a new key on form submission', async () => {
server.use(
rest.post(apiKeysURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(createAPIKeyResponse)),
),
);
fireEvent.click(screen.getByText('New Key'));
const createNewKeyBtn = screen.getByRole('button', {
name: /Create new key/i,
});
await waitFor(() => {
expect(createNewKeyBtn).toBeInTheDocument();
});
act(() => {
const inputElement = screen.getByPlaceholderText('Enter Key Name');
fireEvent.change(inputElement, { target: { value: 'Top Secret' } });
fireEvent.click(screen.getByTestId('create-form-admin-role-btn'));
fireEvent.click(createNewKeyBtn);
});
});
});

View File

@@ -1,874 +0,0 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import {
Avatar,
Button,
Col,
Collapse,
CollapseProps,
Flex,
Form,
Input,
Modal,
Radio,
Row,
Select,
Table,
TableProps,
Tooltip,
Typography,
} from 'antd';
import type { NotificationInstance } from 'antd/es/notification/interface';
import createAPIKeyApi from 'api/v1/pats/create';
import deleteAPIKeyApi from 'api/v1/pats/delete';
import updateAPIKeyApi from 'api/v1/pats/update';
import cx from 'classnames';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys';
import { useNotifications } from 'hooks/useNotifications';
import {
CalendarClock,
Check,
ClipboardEdit,
Contact2,
Copy,
Eye,
Minus,
PenLine,
Plus,
Search,
Trash2,
View,
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { APIKeyProps } from 'types/api/pat/types';
import { USER_ROLES } from 'types/roles';
import './APIKeys.styles.scss';
dayjs.extend(relativeTime);
export const showErrorNotification = (
notifications: NotificationInstance,
err: APIError,
): void => {
notifications.error({
message: err.getErrorCode(),
description: err.getErrorMessage(),
});
};
type ExpiryOption = {
value: string;
label: string;
};
export const EXPIRATION_WITHIN_SEVEN_DAYS = 7;
const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [
{ value: '1', label: '1 day' },
{ value: '7', label: '1 week' },
{ value: '30', label: '1 month' },
{ value: '90', label: '3 months' },
{ value: '365', label: '1 year' },
{ value: '0', label: 'No Expiry' },
];
export const isExpiredToken = (expiryTimestamp: number): boolean => {
if (expiryTimestamp === 0) {
return false;
}
const currentTime = dayjs();
const tokenExpiresAt = dayjs.unix(expiryTimestamp);
return tokenExpiresAt.isBefore(currentTime);
};
export const getDateDifference = (
createdTimestamp: number,
expiryTimestamp: number,
): number => {
const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp);
// Convert seconds to days
return differenceInSeconds / (60 * 60 * 24);
};
function APIKeys(): JSX.Element {
const { user } = useAppContext();
const { notifications } = useNotifications();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [showNewAPIKeyDetails, setShowNewAPIKeyDetails] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [activeAPIKey, setActiveAPIKey] = useState<APIKeyProps | null>();
const [searchValue, setSearchValue] = useState<string>('');
const [dataSource, setDataSource] = useState<APIKeyProps[]>([]);
const { t } = useTranslation(['apiKeys']);
const [editForm] = Form.useForm();
const [createForm] = Form.useForm();
const handleFormReset = (): void => {
editForm.resetFields();
createForm.resetFields();
};
const hideDeleteViewModal = (): void => {
handleFormReset();
setActiveAPIKey(null);
setIsDeleteModalOpen(false);
};
const showDeleteModal = (apiKey: APIKeyProps): void => {
setActiveAPIKey(apiKey);
setIsDeleteModalOpen(true);
};
const hideEditViewModal = (): void => {
handleFormReset();
setActiveAPIKey(null);
setIsEditModalOpen(false);
};
const hideAddViewModal = (): void => {
handleFormReset();
setShowNewAPIKeyDetails(false);
setActiveAPIKey(null);
setIsAddModalOpen(false);
};
const showEditModal = (apiKey: APIKeyProps): void => {
handleFormReset();
setActiveAPIKey(apiKey);
editForm.setFieldsValue({
name: apiKey.name,
role: apiKey.role || USER_ROLES.VIEWER,
});
setIsEditModalOpen(true);
};
const showAddModal = (): void => {
setActiveAPIKey(null);
setIsAddModalOpen(true);
};
const handleModalClose = (): void => {
setActiveAPIKey(null);
};
const {
data: APIKeys,
isLoading,
isRefetching,
refetch: refetchAPIKeys,
error,
isError,
} = useGetAllAPIKeys();
useEffect(() => {
setActiveAPIKey(APIKeys?.data?.[0]);
}, [APIKeys]);
useEffect(() => {
setDataSource(APIKeys?.data || []);
}, [APIKeys?.data]);
useEffect(() => {
if (isError) {
showErrorNotification(notifications, error as APIError);
}
}, [error, isError, notifications]);
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setSearchValue(e.target.value);
const filteredData = APIKeys?.data?.filter(
(key: APIKeyProps) =>
key &&
key.name &&
key.name.toLowerCase().includes(e.target.value.toLowerCase()),
);
setDataSource(filteredData || []);
};
const clearSearch = (): void => {
setSearchValue('');
};
const { mutate: createAPIKey, isLoading: isLoadingCreateAPIKey } = useMutation(
createAPIKeyApi,
{
onSuccess: (data) => {
setShowNewAPIKeyDetails(true);
setActiveAPIKey(data.data);
refetchAPIKeys();
},
onError: (error) => {
showErrorNotification(notifications, error as APIError);
},
},
);
const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation(
updateAPIKeyApi,
{
onSuccess: () => {
refetchAPIKeys();
setIsEditModalOpen(false);
},
onError: (error) => {
showErrorNotification(notifications, error as APIError);
},
},
);
const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation(
deleteAPIKeyApi,
{
onSuccess: () => {
refetchAPIKeys();
setIsDeleteModalOpen(false);
},
onError: (error) => {
showErrorNotification(notifications, error as APIError);
},
},
);
const onDeleteHandler = (): void => {
clearSearch();
if (activeAPIKey) {
deleteAPIKey(activeAPIKey.id);
}
};
const onUpdateApiKey = (): void => {
editForm
.validateFields()
.then((values) => {
if (activeAPIKey) {
updateAPIKey({
id: activeAPIKey.id,
data: {
name: values.name,
role: values.role,
},
});
}
})
.catch((errorInfo) => {
console.error('error info', errorInfo);
});
};
const onCreateAPIKey = (): void => {
createForm
.validateFields()
.then((values) => {
if (user) {
createAPIKey({
name: values.name,
expiresInDays: parseInt(values.expiration, 10),
role: values.role,
});
}
})
.catch((errorInfo) => {
console.error('error info', errorInfo);
});
};
const handleCopyKey = (text: string): void => {
handleCopyToClipboard(text);
notifications.success({
message: 'Copied to clipboard',
});
};
const getFormattedTime = (epochTime: number): string => {
const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const formattedTime = new Date(epochTime * 1000).toLocaleTimeString(
'en-US',
timeOptions,
);
const dateOptions: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const formattedDate = new Date(epochTime * 1000).toLocaleDateString(
'en-US',
dateOptions,
);
return `${formattedDate} ${formattedTime}`;
};
const handleCopyClose = (): void => {
if (activeAPIKey) {
handleCopyKey(activeAPIKey?.token);
}
hideAddViewModal();
};
const columns: TableProps<APIKeyProps>['columns'] = [
{
title: 'API Key',
key: 'api-key',
// eslint-disable-next-line sonarjs/cognitive-complexity
render: (APIKey: APIKeyProps): JSX.Element => {
const formattedDateAndTime =
APIKey && APIKey?.lastUsed && APIKey?.lastUsed !== 0
? getFormattedTime(APIKey?.lastUsed)
: 'Never';
const createdOn = new Date(APIKey.createdAt).toLocaleString();
const expiresIn =
APIKey.expiresAt === 0
? Number.POSITIVE_INFINITY
: getDateDifference(
new Date(APIKey?.createdAt).getTime() / 1000,
APIKey?.expiresAt,
);
const isExpired = isExpiredToken(APIKey.expiresAt);
const expiresOn =
!APIKey.expiresAt || APIKey.expiresAt === 0
? 'No Expiry'
: getFormattedTime(APIKey.expiresAt);
const updatedOn =
!APIKey.updatedAt || APIKey.updatedAt === ''
? null
: new Date(APIKey.updatedAt).toLocaleString();
const items: CollapseProps['items'] = [
{
key: '1',
label: (
<div className="title-with-action">
<div className="api-key-data">
<div className="api-key-title">
<Typography.Text>{APIKey?.name}</Typography.Text>
</div>
<div className="api-key-value">
<Typography.Text>
{APIKey?.token.substring(0, 2)}********
{APIKey?.token.substring(APIKey.token.length - 2).trim()}
</Typography.Text>
<Copy
className="copy-key-btn"
size={12}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
handleCopyKey(APIKey.token);
}}
/>
</div>
{APIKey.role === USER_ROLES.ADMIN && (
<Tooltip title={USER_ROLES.ADMIN}>
<Contact2 size={14} color={Color.BG_ROBIN_400} />
</Tooltip>
)}
{APIKey.role === USER_ROLES.EDITOR && (
<Tooltip title={USER_ROLES.EDITOR}>
<ClipboardEdit size={14} color={Color.BG_ROBIN_400} />
</Tooltip>
)}
{APIKey.role === USER_ROLES.VIEWER && (
<Tooltip title={USER_ROLES.VIEWER}>
<View size={14} color={Color.BG_ROBIN_400} />
</Tooltip>
)}
{!APIKey.role && (
<Tooltip title={USER_ROLES.ADMIN}>
<Contact2 size={14} color={Color.BG_ROBIN_400} />
</Tooltip>
)}
</div>
<div className="action-btn">
<Button
className="periscope-btn ghost"
icon={<PenLine size={14} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
showEditModal(APIKey);
}}
/>
<Button
className="periscope-btn ghost"
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
showDeleteModal(APIKey);
}}
/>
</div>
</div>
),
children: (
<div className="api-key-info-container">
{APIKey?.createdByUser && (
<Row>
<Col span={6}> Creator </Col>
<Col span={12} className="user-info">
<Avatar className="user-avatar" size="small">
{APIKey?.createdByUser?.displayName?.substring(0, 1)}
</Avatar>
<Typography.Text>
{APIKey.createdByUser?.displayName}
</Typography.Text>
<div className="user-email">{APIKey.createdByUser?.email}</div>
</Col>
</Row>
)}
<Row>
<Col span={6}> Created on </Col>
<Col span={12}>
<Typography.Text>{createdOn}</Typography.Text>
</Col>
</Row>
{updatedOn && (
<Row>
<Col span={6}> Updated on </Col>
<Col span={12}>
<Typography.Text>{updatedOn}</Typography.Text>
</Col>
</Row>
)}
<Row>
<Col span={6}> Expires on </Col>
<Col span={12}>
<Typography.Text>{expiresOn}</Typography.Text>
</Col>
</Row>
</div>
),
},
];
return (
<div className="column-render">
<Collapse items={items} />
<div className="api-key-details">
<div className="api-key-last-used-at">
<CalendarClock size={14} />
Last used <Minus size={12} />
<Typography.Text>{formattedDateAndTime}</Typography.Text>
</div>
{!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && (
<div
className={cx(
'api-key-expires-in',
expiresIn <= 3 ? 'danger' : 'warning',
)}
>
<span className="dot" /> Expires {dayjs().to(expiresOn)}
</div>
)}
{isExpired && (
<div className={cx('api-key-expires-in danger')}>
<span className="dot" /> Expired
</div>
)}
</div>
</div>
);
},
},
];
return (
<div className="api-key-container">
<div className="api-key-content">
<header>
<Typography.Title className="title">API Keys</Typography.Title>
<Typography.Text className="subtitle">
Create and manage API keys for the SigNoz API
</Typography.Text>
</header>
<div className="api-keys-search-add-new">
<Input
placeholder="Search for keys..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
/>
<Button
className="add-new-api-key-btn"
type="primary"
onClick={showAddModal}
>
<Plus size={14} /> New Key
</Button>
</div>
<Table
columns={columns}
dataSource={dataSource}
loading={isLoading || isRefetching}
showHeader={false}
pagination={{
pageSize: 5,
hideOnSinglePage: true,
showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} keys`,
}}
/>
</div>
{/* Delete Key Modal */}
<Modal
className="delete-api-key-modal"
title={<span className="title">Delete Key</span>}
open={isDeleteModalOpen}
closable
afterClose={handleModalClose}
onCancel={hideDeleteViewModal}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={hideDeleteViewModal}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
icon={<Trash2 size={16} />}
loading={isDeleteingAPIKey}
onClick={onDeleteHandler}
className="delete-btn"
>
Delete key
</Button>,
]}
>
<Typography.Text className="delete-text">
{t('delete_confirm_message', {
keyName: activeAPIKey?.name,
})}
</Typography.Text>
</Modal>
{/* Edit Key Modal */}
<Modal
className="api-key-modal"
title="Edit key"
open={isEditModalOpen}
key="edit-api-key-modal"
afterClose={handleModalClose}
// closable
onCancel={hideEditViewModal}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={hideEditViewModal}
className="periscope-btn cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
className="periscope-btn primary"
key="submit"
type="primary"
loading={isLoadingUpdateAPIKey}
icon={<Check size={14} />}
onClick={onUpdateApiKey}
>
Update key
</Button>,
]}
>
<Form
name="edit-api-key-form"
key={activeAPIKey?.id}
form={editForm}
layout="vertical"
autoComplete="off"
initialValues={{
name: activeAPIKey?.name,
role: activeAPIKey?.role,
}}
>
<Form.Item
name="name"
label="Name"
rules={[{ required: true }, { type: 'string', min: 6 }]}
>
<Input placeholder="Enter Key Name" autoFocus />
</Form.Item>
<Form.Item name="role" label="Role">
<Flex vertical gap="middle">
<Radio.Group
buttonStyle="solid"
className="api-key-access-role"
defaultValue={activeAPIKey?.role}
>
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
<div className="role">
<Contact2 size={14} /> Admin
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.EDITOR} className={cx('tab')}>
<div className="role">
<ClipboardEdit size={14} /> Editor
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.VIEWER} className={cx('tab')}>
<div className="role">
<Eye size={14} /> Viewer
</div>
</Radio.Button>
</Radio.Group>
</Flex>
</Form.Item>
</Form>
</Modal>
{/* Create New Key Modal */}
<Modal
className="api-key-modal"
title="Create new key"
open={isAddModalOpen}
key="create-api-key-modal"
closable
onCancel={hideAddViewModal}
destroyOnClose
footer={
showNewAPIKeyDetails
? [
<Button
key="copy-key-close"
className="periscope-btn primary"
data-testid="copy-key-close-btn"
type="primary"
onClick={handleCopyClose}
icon={<Check size={12} />}
>
Copy key and close
</Button>,
]
: [
<Button
key="cancel"
onClick={hideAddViewModal}
className="periscope-btn cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
className="periscope-btn primary"
test-id="create-new-key"
key="submit"
type="primary"
icon={<Check size={14} />}
loading={isLoadingCreateAPIKey}
onClick={onCreateAPIKey}
>
Create new key
</Button>,
]
}
>
{!showNewAPIKeyDetails && (
<Form
key="createForm"
name="create-api-key-form"
form={createForm}
initialValues={{
role: USER_ROLES.ADMIN,
expiration: '1',
name: '',
}}
layout="vertical"
autoComplete="off"
>
<Form.Item
name="name"
label="Name"
rules={[{ required: true }, { type: 'string', min: 6 }]}
validateTrigger="onFinish"
>
<Input placeholder="Enter Key Name" autoFocus />
</Form.Item>
<Form.Item name="role" label="Role">
<Flex vertical gap="middle">
<Radio.Group
buttonStyle="solid"
className="api-key-access-role"
defaultValue={USER_ROLES.ADMIN}
>
<Radio.Button value={USER_ROLES.ADMIN} className={cx('tab')}>
<div className="role" data-testid="create-form-admin-role-btn">
<Contact2 size={14} /> Admin
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.EDITOR} className="tab">
<div className="role" data-testid="create-form-editor-role-btn">
<ClipboardEdit size={14} /> Editor
</div>
</Radio.Button>
<Radio.Button value={USER_ROLES.VIEWER} className="tab">
<div className="role" data-testid="create-form-viewer-role-btn">
<Eye size={14} /> Viewer
</div>
</Radio.Button>
</Radio.Group>
</Flex>
</Form.Item>
<Form.Item name="expiration" label="Expiration">
<Select
className="expiration-selector"
placeholder="Expiration"
options={API_KEY_EXPIRY_OPTIONS}
/>
</Form.Item>
</Form>
)}
{showNewAPIKeyDetails && (
<div className="api-key-info-container">
<Row>
<Col span={8}>Key</Col>
<Col span={16}>
<span className="copyable-text">
<Typography.Text>
{activeAPIKey?.token.substring(0, 2)}****************
{activeAPIKey?.token.substring(activeAPIKey.token.length - 2).trim()}
</Typography.Text>
<Copy
className="copy-key-btn"
size={12}
onClick={(): void => {
if (activeAPIKey) {
handleCopyKey(activeAPIKey.token);
}
}}
/>
</span>
</Col>
</Row>
<Row>
<Col span={8}>Name</Col>
<Col span={16}>{activeAPIKey?.name}</Col>
</Row>
<Row>
<Col span={8}>Role</Col>
<Col span={16}>
{activeAPIKey?.role === USER_ROLES.ADMIN && (
<div className="role">
<Contact2 size={14} /> Admin
</div>
)}
{activeAPIKey?.role === USER_ROLES.EDITOR && (
<div className="role">
{' '}
<ClipboardEdit size={14} /> Editor
</div>
)}
{activeAPIKey?.role === USER_ROLES.VIEWER && (
<div className="role">
{' '}
<View size={14} /> Viewer
</div>
)}
</Col>
</Row>
<Row>
<Col span={8}>Creator</Col>
<Col span={16} className="user-info">
<Avatar className="user-avatar" size="small">
{activeAPIKey?.createdByUser?.displayName?.substring(0, 1)}
</Avatar>
<Typography.Text>
{activeAPIKey?.createdByUser?.displayName}
</Typography.Text>
<div className="user-email">{activeAPIKey?.createdByUser?.email}</div>
</Col>
</Row>
{activeAPIKey?.createdAt && (
<Row>
<Col span={8}>Created on</Col>
<Col span={16}>
{new Date(activeAPIKey?.createdAt).toLocaleString()}
</Col>
</Row>
)}
{activeAPIKey?.expiresAt !== 0 && activeAPIKey?.expiresAt && (
<Row>
<Col span={8}>Expires on</Col>
<Col span={16}>{getFormattedTime(activeAPIKey?.expiresAt)}</Col>
</Row>
)}
{activeAPIKey?.expiresAt === 0 && (
<Row>
<Col span={8}>Expires on</Col>
<Col span={16}> No Expiry </Col>
</Row>
)}
</div>
)}
</Modal>
</div>
);
}
export default APIKeys;

View File

@@ -16,9 +16,9 @@ function AverageResolutionCard({
}: TotalTriggeredCardProps): JSX.Element {
return (
<StatsCard
displayValue={formatTime(currentAvgResolutionTime)}
totalCurrentCount={currentAvgResolutionTime}
totalPastCount={pastAvgResolutionTime}
displayValue={formatTime(+currentAvgResolutionTime)}
totalCurrentCount={+currentAvgResolutionTime}
totalPastCount={+pastAvgResolutionTime}
title="Avg. Resolution Time"
timeSeries={timeSeries}
/>

View File

@@ -800,14 +800,10 @@
.ant-table-cell:has(.top-services-item-latency) {
text-align: center;
opacity: 0.8;
background: rgba(171, 189, 255, 0.04);
}
.ant-table-cell:has(.top-services-item-latency-title) {
text-align: center;
opacity: 0.8;
background: rgba(171, 189, 255, 0.04);
}
.ant-table-tbody > tr:hover > td {

View File

@@ -9,7 +9,6 @@
.api-quick-filters-header {
padding: 12px;
border-bottom: 1px solid var(--bg-slate-400);
border-right: 1px solid var(--bg-slate-400);
display: flex;
align-items: center;
@@ -26,6 +25,7 @@
width: 100%;
.toolbar {
border-top: 0px;
border-bottom: 1px solid var(--bg-slate-400);
}
@@ -220,6 +220,18 @@
}
.lightMode {
.api-quick-filter-left-section {
.api-quick-filters-header {
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
.api-module-right-section {
.toolbar {
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
.no-filtered-domains-message-container {
.no-filtered-domains-message-content {
.no-filtered-domains-message {

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(

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