Compare commits

...

13 Commits

Author SHA1 Message Date
aks07
d97e579f5e feat(quick-filters): fetch values from fields/values (CheckboxV2) on all pages
Signal pages (logs, traces, exceptions, api monitoring, meter) now render
CheckboxV2 so quick filter values come from fields/values instead of the old v3
attribute_values. Added a shared useSignalFieldApis hook for the time range;
related values stay off (infra only). Meter's source is derived from the quick
filter source inside CheckboxV2. Bool fields synthesize true/false since the api
returns empty for them.
2026-09-03 13:35:00 +05:30
aks07
f8821b73aa fix(quick-filters): follow backend rename to /api/v2/quick_filters
The backend moved the endpoint from /orgs/me/filters to quick_filters and the
generated hook from useGetSignalQuickFilters to useGetQuickFilters. Point our
load hook and test mocks at the new names.
2026-09-03 13:35:00 +05:30
aks07
7b7fd139d1 test(quick-filters): update tests and mocks for the new field APIs
Point the /me/filters and suggestions mocks at the v2 and fields/keys endpoints
and update the fixture shapes. Adds a test that same-name fields with different
context/datatype stay distinct (both addable, independently removable).
2026-09-03 13:35:00 +05:30
aks07
ba0ffd36d1 feat(quick-filters): migrate to new field APIs (fields/keys + v2 /me/filters)
Quick filters now run on the new field apis. Keys come from fields/keys for all
pages (logs, traces, exceptions, api monitoring, meter), and save/load moves to
the v2 /me/filters that stores TelemetryFieldKey. Dropped the old hand-written
v1 clients.

Filters are now identified by name + fieldContext + fieldDataType, so same-name
fields with different context/datatype don't clash.

Values still use the old v3 path...that migration comes next.
2026-09-03 13:35:00 +05:30
Nityananda Gohain
a75442f31e fix: add quick filters v2 api to support TelemetryFieldKey (#12698)
Some checks failed
build-staging / staging (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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description
The old API didn't support telemetryFieldKey, so adding a new v2 API to
support it.

This PR
* Migrates old data to the new one.
* Existing API's now internally stores it in the new struct so that they
don't break the UI.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Closes https://github.com/SigNoz/engineering-pod/issues/5947

## Additional details
* the old api is safe with new field as it is just a subset of it.
2026-09-03 07:49:31 +00:00
Nityananda Gohain
52588c4582 fix: support for related values in ai field values (#12716)
#### Description

Adds support for related values in ai observability field values.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/5975
2026-09-03 07:12:22 +00:00
Nityananda Gohain
e485f50221 feat: support ai trace aggregate filtering in ai span list (#12122)
## Pull Request

---

### 📄 Summary
* Span list (raw) queries can now leverage trace-level `trace.` filter
conditions leading to the trace-level component being qualified with
`__trace_scope`.
* An error is now raised if the span list is ordered by the trace level
key.


The following constraint is known: in case of ordering by `timestamp`,
the querier processes the span list by time bucket thus performing
trace-level aggregates calculation per bucket not within a time window.
The records are kept separately.


#### Issues closed by this PR

Fixes https://github.com/SigNoz/engineering-pod/issues/5976

---

###  Change Type
_Select all that apply_

- [ ]  Feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactor
- [ ] 🛠️ Infra / Tooling
- [ ] 🧪 Test-only

---

### 🧪 Testing Strategy
> How was this change validated?

- Tests added/updated: 
- Manual verification: 
- Edge cases covered:

---

### ⚠️ Risk & Impact Assessment
> What could break? How do we recover?

- Blast radius: None
- Potential regressions:
- Rollback plan:
2026-09-03 07:10:24 +00:00
Vikrant Gupta
b3b547f34a feat(gateway): add first-class ingestion limit APIs (#12625)
#### Description

- Adds first-class ingestion limit APIs under
`/api/v2/gateway/ingestion_limits`: create (`keyId` in body), get,
update, and delete by `{limitId}`. Get proxies the new upstream `GET
/v1/workspaces/me/limits/{limitID}`.
- Adds key read APIs: `GET /api/v2/gateway/ingestion_keys/{keyId}` (key
by id — upstream does not embed limits here) and `GET
/api/v2/gateway/ingestion_keys/{keyId}/limits` (limits for a key, with
current-period usage metrics).
- Marks the existing limit routes (`POST
/ingestion_keys/{keyId}/limits`, `PATCH/DELETE
/ingestion_keys/limits/{limitId}`) as deprecated; they keep working
unchanged.
- Renames the old create body to `DeprecatedPostableIngestionKeyLimit`;
`PostableIngestionKeyLimit` is now the first-class body carrying
`keyId`. Handlers decode via `binding.JSON` and the create response is
`types.Identifiable`.

Part of SigNoz/platform-pod#2651.

#### Additional Information

- OpenAPI spec and the generated frontend client are regenerated; the UI
stays on the deprecated routes for now.
- Requires the upstream get-by-id endpoints from
SigNoz/opentelemetry-gateway#96 (merged and deployed).
2026-09-03 07:07:10 +00:00
Naman Verma
be9a045455 chore: add internal name to data layer of notification channels (#12754)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Convert the existing `name` to store an immutable DNS1123 internal name
of a notification_channel, and add a display name column where the data
from the existing `name` column will go.

This is just a database level change. The internal name is not being
used by any consumer, be it the API or rules or route policies. All that
will come in subsequent PRs

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Part of https://github.com/SigNoz/pulse-pod/issues/296

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

Eventually references (rules, routing policies) migrate onto the
internal name, freeing the display name to become a user-editable. But
that will happen post rules migration so that all rules are on v2.


<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-03 06:52:58 +00:00
Nikhil Mantri
297d3dd44b feat(alert-channel-integrations): incidentio frontend (#12645)
## Description

Frontend for the **incident.io** alert channel (backend in #12644 — this
PR is stacked on it).

- Adds **incident.io** to the channel-type dropdown with a settings
form: alert source **URL** + **token** (both required), and
**title/description** template fields prefilled with the backend
defaults — same UX as Jira/JSM.
- A tip above the form links to the setup docs (create an HTTP alert
source in incident.io, copy URL + token).
- Client-side validation mirrors the backend for a nicer error
experience: both fields required, URL must be an alert events URL
(`…/v2/alert_events/http/<source_config_id>`).
- `send_resolved` is seeded **on** so incident.io alerts resolve with
the rule (backend can't default it — same reasoning as JSM).
- **Additional metadata** section: key-value rows merged into every
alert's metadata on top of the alert's labels (channel wins on clash);
values may use templates.
- Create, edit and test-channel flows all wired; editing prefills from
the stored `incidentio_configs`.

Notes for the reviewer:

- Follows the JSM Ops form/handler pattern file-for-file; no new
patterns introduced.
- Tests: 4 create-flow cases (fields render, required-field error, URL
validation error, payload shape with defaults) + 1 edit-flow payload
case.

## Issues closed by this PR

Closes SigNoz/pulse-pod#173

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-09-03 06:20:47 +00:00
Nikhil Mantri
e84a61d43f feat(alert-channel-integrations): incidentio channel integration (#12644)
## Description

Adds **incident.io** as a native alert notification channel, using
incident.io's HTTP alert source (Alert Events V2 API)

- A channel is configured with the alert source's **URL + token**; title
and description templates are prefilled with the same defaults as
Jira/JSM.
- Alerts fire and auto-resolve in incident.io; the description is
markdown (incident.io renders it natively) and carries the usual deep
links — **View in SigNoz, related logs, related traces**.
- All rule labels (severity, team, custom labels) are sent as
**metadata**, so users can map them to incident.io attributes and
route/escalate on them.

Notes and decisions for the reviewer (full details in the [discussion
ticket and doc](https://github.com/SigNoz/pulse-pod/issues/171)):

- **Dedup:** one incident.io alert per notification group, keyed by the
group key hash (same identity Jira uses). A resolve targets the same
key; re-fires after resolve correctly open a fresh alert — no key
rotation needed.
- **Repeat notifications are no-ops on incident.io** (it drops duplicate
firing events) — unlike Jira, we cannot append updated values to an open
alert; operators click through to SigNoz for current values.
- **Limits:** description capped client-side under incident.io's
documented 512 KB payload limit; retries only on 429/5xx (documented
limit: 120 events/min per source).
- **Channel-level metadata:** optional key-value pairs on the channel
config, merged into every event's metadata on top of the alert's labels
(channel wins on key clash — Opsgenie precedent). Values are
template-expanded; a value that fails to expand is sent raw with a
warning logged, so delivery never breaks on a bad template.
- Upstream alertmanager ships its own basic incident.io notifier — our
config **shadows it** so the SigNoz notifier (templates, dedup,
metadata) handles delivery.
- Frontend (channel form) follows in a stacked PR.

## Issues closed by this PR

Closes SigNoz/pulse-pod#172

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-09-03 05:32:51 +00:00
Vikrant Gupta
afb1eb4a41 feat(licensing): add v4 license endpoints with resource authz (#12731)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Adds strongly typed `/api/v4/licenses` endpoints on the apiserver with
OpenAPI definitions: activate, list, get, refresh, delete, and `GET
/api/v4/licenses/active`.
- Wires resource authz (`license:create/list/read/update/delete`) via
`CheckResources`; `GET /active` is `OpenAccess` and never includes the
license key — the key is returned only by the FGA-gated get-by-id, so
orgs can grant `license:read` selectively. Migration 118 backfills
license tuples for existing orgs.
- Delete is allowed only for non-cloud licenses; licenses managed by
SigNoz Cloud are rejected.
- v4 responses are camelCase with lowercase enum values; v3 routes and
stored license data are unchanged.

#### Issues closed by this PR

Closes https://github.com/SigNoz/platform-pod/issues/3076
2026-09-02 18:49:35 +00:00
Aditya Singh
4f6414ef61 fix(logs): preserve active viewKey in log-details filter/group/replace (#12757)
Some checks failed
build-staging / staging (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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Fixes the issue where applying filter for value / filter out (also
group by and replace filter) from the log details drawer was removing
the added columns from the table.
- As part of this fix, we rename `id` and `name` fields in
explorerTabChange input type. Now it reads `viewName` and `viewKey` and
avoids confusion
- Now consumers of explorerTabChange need to send viewName and viewKey
only if needed. As now this is an optional field

#### Screenshots/Recording

Before


https://github.com/user-attachments/assets/4c6145e3-4979-4386-9230-f74eb00721c8



After


https://github.com/user-attachments/assets/52bc3083-40a2-4345-aebd-63770882b06c



#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/6012

#### Additional Information

- same issue was present in the old drawer (group by, replace filter)
and the metrics explorer detail (passed the metric name)..both fixed
here. metrics never surfaced as a bug since it opens in time series with
no columns to collapse
- filter for/out in the old drawer was never affected, it uses a
different add to query path
2026-09-02 12:13:37 +00:00
154 changed files with 9200 additions and 1734 deletions

View File

@@ -50,6 +50,7 @@ jobs:
- logspipelines
- passwordauthn
- preference
- quickfilter
- querierlogs
- queriertraces
- queriermetrics

View File

@@ -96,6 +96,7 @@ func runGenerateAuthz(_ context.Context) error {
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceLicense).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,

File diff suppressed because it is too large Load Diff

View File

@@ -108,6 +108,20 @@ func (provider *Provider) SearchIngestionKeysByName(ctx context.Context, orgID v
}, nil
}
func (provider *Provider) GetIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string) (*gatewaytypes.IngestionKey, error) {
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/"+keyID, nil, nil)
if err != nil {
return nil, err
}
var ingestionKey gatewaytypes.IngestionKey
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKey); err != nil {
return nil, err
}
return &ingestionKey, nil
}
func (provider *Provider) CreateIngestionKey(ctx context.Context, orgID valuer.UUID, name string, tags []string, expiresAt time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error) {
requestBody := gatewaytypes.PostableIngestionKey{
Name: name,
@@ -161,7 +175,7 @@ func (provider *Provider) DeleteIngestionKey(ctx context.Context, orgID valuer.U
}
func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, keyID string, signal string, limitConfig gatewaytypes.LimitConfig, tags []string) (*gatewaytypes.GettableCreatedIngestionKeyLimit, error) {
requestBody := gatewaytypes.PostableIngestionKeyLimit{
requestBody := gatewaytypes.DeprecatedPostableIngestionKeyLimit{
Signal: signal,
Config: limitConfig,
Tags: tags,
@@ -184,6 +198,34 @@ func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID val
return &createdIngestionKeyLimitResponse, nil
}
func (provider *Provider) GetIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string) (*gatewaytypes.Limit, error) {
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/limits/"+limitID, nil, nil)
if err != nil {
return nil, err
}
var limit gatewaytypes.Limit
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &limit); err != nil {
return nil, err
}
return &limit, nil
}
func (provider *Provider) GetIngestionKeyLimits(ctx context.Context, orgID valuer.UUID, keyID string) ([]gatewaytypes.Limit, error) {
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/"+keyID+"/limits", nil, nil)
if err != nil {
return nil, err
}
var limits []gatewaytypes.Limit
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &limits); err != nil {
return nil, err
}
return limits, nil
}
func (provider *Provider) UpdateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string, limitConfig gatewaytypes.LimitConfig, tags []string) error {
requestBody := gatewaytypes.UpdatableIngestionKeyLimit{
Config: limitConfig,

View File

@@ -22,89 +22,6 @@ func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
return &licensingAPI{licensing: licensing}
}
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableLicense)
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
err = api.licensing.Activate(r.Context(), orgID, req.Key)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
license, err := api.licensing.GetActive(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
render.Success(rw, http.StatusOK, gettableLicense)
}
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
err = api.licensing.Refresh(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -95,24 +95,65 @@ func (provider *provider) Validate(ctx context.Context) error {
return nil
}
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
data, err := provider.zeus.GetLicense(ctx, key)
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) (*licensetypes.License, error) {
zeusLicense, err := provider.zeus.GetLicense(ctx, key)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
}
license, err := licensetypes.NewLicense(data, organizationID)
license, err := licensetypes.NewLicense(zeusLicense, organizationID)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
}
storableLicense := licensetypes.NewStorableLicenseFromLicense(license)
err = provider.store.Create(ctx, storableLicense)
if err != nil {
return nil, err
}
return license, nil
}
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error) {
storableLicense, err := provider.store.Get(ctx, organizationID, licenseID)
if err != nil {
return nil, err
}
return licensetypes.NewLicenseFromStorableLicense(storableLicense)
}
func (provider *provider) List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error) {
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
if err != nil {
return nil, err
}
licenses := make([]*licensetypes.License, 0, len(storableLicenses))
for _, storableLicense := range storableLicenses {
license, err := licensetypes.NewLicenseFromStorableLicense(storableLicense)
if err != nil {
return nil, err
}
licenses = append(licenses, license)
}
return licenses, nil
}
func (provider *provider) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
license, err := provider.Get(ctx, organizationID, licenseID)
if err != nil {
return err
}
return nil
if err := license.ErrIfCloud(); err != nil {
return errors.WithAdditionalf(err, "license %s cannot be deleted", licenseID.StringValue())
}
return provider.store.Delete(ctx, organizationID, licenseID)
}
func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
@@ -139,7 +180,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
zeusLicense, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
@@ -154,7 +195,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
err = activeLicense.Update(data)
err = activeLicense.Update(zeusLicense)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
}

View File

@@ -64,6 +64,22 @@ func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*
return storableLicenses, nil
}
func (store *store) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(licensetypes.StorableLicense)).
Where("org_id = ?", organizationID).
Where("id = ?", licenseID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to delete license with ID: %s", licenseID)
}
return nil
}
func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
_, err := store.
sqlstore.

View File

@@ -76,11 +76,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)

View File

@@ -51,7 +51,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
}, nil
}
func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, error) {
func (provider *Provider) GetLicense(ctx context.Context, key string) (*zeustypes.License, error) {
response, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v2/licenses/me"),
@@ -63,7 +63,12 @@ func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, e
return nil, err
}
return []byte(gjson.GetBytes(response, "data").String()), nil
license := new(zeustypes.License)
if err := json.Unmarshal([]byte(gjson.GetBytes(response, "data").String()), license); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal license data")
}
return license, nil
}
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string, body []byte) ([]byte, error) {

View File

@@ -75,6 +75,23 @@
"field_jsmops_tags": "Tags",
"placeholder_jsmops_tags": "Type a tag and press Enter",
"help_jsmops_tags": "Tags added to every alert.",
"incidentio_tip": "Create an HTTP alert source in incident.io (On-call \u2192 Alert routing \u2192 Sources) and paste its URL and token below.",
"incidentio_tip_link": "Learn how",
"field_incidentio_url": "Alert source URL",
"help_incidentio_url": "The alert events URL from the source's setup page, e.g. https://api.incident.io/v2/alert_events/http/<source_config_id>.",
"field_incidentio_token": "Token",
"help_incidentio_token": "The alert source's secret token, from the same setup page.",
"field_incidentio_title": "Title",
"help_incidentio_title": "Template for the alert title. Kept stable while the alert fires \u2014 incident.io ignores content updates on repeat events.",
"field_incidentio_description": "Description",
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
"incidentio_required_fields": "Alert source URL and token are required",
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
"field_incidentio_metadata": "Additional metadata",
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
"placeholder_incidentio_metadata_key": "Key",
"placeholder_incidentio_metadata_value": "Value",
"button_incidentio_add_metadata": "Add metadata",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",

View File

@@ -75,6 +75,23 @@
"field_jsmops_tags": "Tags",
"placeholder_jsmops_tags": "Type a tag and press Enter",
"help_jsmops_tags": "Tags added to every alert.",
"incidentio_tip": "Create an HTTP alert source in incident.io (On-call \u2192 Alert routing \u2192 Sources) and paste its URL and token below.",
"incidentio_tip_link": "Learn how",
"field_incidentio_url": "Alert source URL",
"help_incidentio_url": "The alert events URL from the source's setup page, e.g. https://api.incident.io/v2/alert_events/http/<source_config_id>.",
"field_incidentio_token": "Token",
"help_incidentio_token": "The alert source's secret token, from the same setup page.",
"field_incidentio_title": "Title",
"help_incidentio_title": "Template for the alert title. Kept stable while the alert fires \u2014 incident.io ignores content updates on repeat events.",
"field_incidentio_description": "Description",
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
"incidentio_required_fields": "Alert source URL and token are required",
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
"field_incidentio_metadata": "Additional metadata",
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
"placeholder_incidentio_metadata_key": "Key",
"placeholder_incidentio_metadata_value": "Value",
"button_incidentio_add_metadata": "Add metadata",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",

View File

@@ -103,30 +103,30 @@ function createMockLicense(
overrides: Partial<LicenseResModel> = {},
): LicenseResModel {
return {
key: 'test-key',
event_queue: {
created_at: '0',
id: 'test-license-id',
eventQueue: {
createdAt: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
scheduledAt: '0',
status: '',
updated_at: '0',
updatedAt: '0',
},
state: LicenseState.ACTIVATED,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,
created_at: '0',
createdAt: '0',
plan: {
created_at: '0',
id: '0',
createdAt: '0',
description: '',
is_active: true,
isActive: true,
name: '',
updated_at: '0',
updatedAt: '0',
},
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
...overrides,
};
}
@@ -850,6 +850,22 @@ describe('PrivateRoute', () => {
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
});
it('should keep a custom role (ANONYMOUS) on workspace locked instead of bouncing to unauthorized', () => {
renderPrivateRoute({
initialRoute: ROUTES.WORKSPACE_LOCKED,
appContext: {
isLoggedIn: true,
isFetchingActiveLicense: false,
activeLicense: createMockLicense({ platform: LicensePlatform.CLOUD }),
trialInfo: createMockTrialInfo({ workSpaceBlock: true }),
user: createMockUser({ role: USER_ROLES.ANONYMOUS as ROLES }),
},
isCloudUser: true,
});
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
});
it('should not redirect self-hosted users to workspace locked even when workSpaceBlock is true', () => {
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1024,6 +1040,24 @@ describe('PrivateRoute', () => {
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
});
it('should keep a custom role (ANONYMOUS) on workspace suspended instead of bouncing to unauthorized', () => {
renderPrivateRoute({
initialRoute: ROUTES.WORKSPACE_SUSPENDED,
appContext: {
isLoggedIn: true,
isFetchingActiveLicense: false,
activeLicense: createMockLicense({
platform: LicensePlatform.CLOUD,
state: LicenseState.DEFAULTED,
}),
user: createMockUser({ role: USER_ROLES.ANONYMOUS as ROLES }),
},
isCloudUser: true,
});
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
});
it('should not redirect self-hosted users to workspace suspended when license is defaulted', () => {
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1580,6 +1614,18 @@ describe('PrivateRoute', () => {
path: ROUTES.SUPPORT,
deniedRoles: [USER_ROLES.AUTHOR as ROLES],
},
WORKSPACE_LOCKED: {
path: ROUTES.WORKSPACE_LOCKED,
deniedRoles: DENIED_ROLES,
},
WORKSPACE_SUSPENDED: {
path: ROUTES.WORKSPACE_SUSPENDED,
deniedRoles: DENIED_ROLES,
},
WORKSPACE_ACCESS_RESTRICTED: {
path: ROUTES.WORKSPACE_ACCESS_RESTRICTED,
deniedRoles: DENIED_ROLES,
},
};
const authzRouteRolePairs: [string, string, ROLES][] = Object.entries(

View File

@@ -21,18 +21,28 @@ import type {
CreateIngestionKey201,
CreateIngestionKeyLimit201,
CreateIngestionKeyLimitPathParameters,
CreateIngestionLimit201,
DeleteIngestionKeyLimitPathParameters,
DeleteIngestionKeyPathParameters,
DeleteIngestionLimitPathParameters,
GatewaytypesDeprecatedPostableIngestionKeyLimitDTO,
GatewaytypesPostableIngestionKeyDTO,
GatewaytypesPostableIngestionKeyLimitDTO,
GatewaytypesUpdatableIngestionKeyLimitDTO,
GetIngestionKey200,
GetIngestionKeyLimits200,
GetIngestionKeyLimitsPathParameters,
GetIngestionKeyPathParameters,
GetIngestionKeys200,
GetIngestionKeysParams,
GetIngestionLimit200,
GetIngestionLimitPathParameters,
RenderErrorResponseDTO,
SearchIngestionKeys200,
SearchIngestionKeysParams,
UpdateIngestionKeyLimitPathParameters,
UpdateIngestionKeyPathParameters,
UpdateIngestionLimitPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -300,6 +310,108 @@ export const useDeleteIngestionKey = <
> => {
return useMutation(getDeleteIngestionKeyMutationOptions(options));
};
/**
* This endpoint returns an ingestion key for the workspace
* @summary Get ingestion key for workspace
*/
export const getIngestionKey = (
{ keyId }: GetIngestionKeyPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionKey200>({
url: `/api/v2/gateway/ingestion_keys/${keyId}`,
method: 'GET',
signal,
});
};
export const getGetIngestionKeyQueryKey = ({
keyId,
}: GetIngestionKeyPathParameters) => {
return [`/api/v2/gateway/ingestion_keys/${keyId}`] as const;
};
export const getGetIngestionKeyQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionKey>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetIngestionKeyQueryKey({ keyId });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getIngestionKey>>> = ({
signal,
}) => getIngestionKey({ keyId }, signal);
return {
queryKey,
queryFn,
enabled: !!keyId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetIngestionKeyQueryResult = NonNullable<
Awaited<ReturnType<typeof getIngestionKey>>
>;
export type GetIngestionKeyQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get ingestion key for workspace
*/
export function useGetIngestionKey<
TData = Awaited<ReturnType<typeof getIngestionKey>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeyQueryOptions({ keyId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get ingestion key for workspace
*/
export const invalidateGetIngestionKey = async (
queryClient: QueryClient,
{ keyId }: GetIngestionKeyPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionKeyQueryKey({ keyId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an ingestion key for the workspace
* @summary Update ingestion key for workspace
@@ -399,20 +511,123 @@ export const useUpdateIngestionKey = <
> => {
return useMutation(getUpdateIngestionKeyMutationOptions(options));
};
/**
* This endpoint returns the ingestion limits for an ingestion key
* @summary Get limits for the ingestion key
*/
export const getIngestionKeyLimits = (
{ keyId }: GetIngestionKeyLimitsPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionKeyLimits200>({
url: `/api/v2/gateway/ingestion_keys/${keyId}/limits`,
method: 'GET',
signal,
});
};
export const getGetIngestionKeyLimitsQueryKey = ({
keyId,
}: GetIngestionKeyLimitsPathParameters) => {
return [`/api/v2/gateway/ingestion_keys/${keyId}/limits`] as const;
};
export const getGetIngestionKeyLimitsQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyLimitsPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetIngestionKeyLimitsQueryKey({ keyId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getIngestionKeyLimits>>
> = ({ signal }) => getIngestionKeyLimits({ keyId }, signal);
return {
queryKey,
queryFn,
enabled: !!keyId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetIngestionKeyLimitsQueryResult = NonNullable<
Awaited<ReturnType<typeof getIngestionKeyLimits>>
>;
export type GetIngestionKeyLimitsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get limits for the ingestion key
*/
export function useGetIngestionKeyLimits<
TData = Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyLimitsPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeyLimitsQueryOptions({ keyId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get limits for the ingestion key
*/
export const invalidateGetIngestionKeyLimits = async (
queryClient: QueryClient,
{ keyId }: GetIngestionKeyLimitsPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionKeyLimitsQueryKey({ keyId }) },
options,
);
return queryClient;
};
/**
* This endpoint creates an ingestion key limit
* @deprecated
* @summary Create limit for the ingestion key
*/
export const createIngestionKeyLimit = (
{ keyId }: CreateIngestionKeyLimitPathParameters,
gatewaytypesPostableIngestionKeyLimitDTO?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>,
gatewaytypesDeprecatedPostableIngestionKeyLimitDTO?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateIngestionKeyLimit201>({
url: `/api/v2/gateway/ingestion_keys/${keyId}/limits`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: gatewaytypesPostableIngestionKeyLimitDTO,
data: gatewaytypesDeprecatedPostableIngestionKeyLimitDTO,
signal,
});
};
@@ -426,7 +641,7 @@ export const getCreateIngestionKeyLimitMutationOptions = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
>;
@@ -435,7 +650,7 @@ export const getCreateIngestionKeyLimitMutationOptions = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
> => {
@@ -452,7 +667,7 @@ export const getCreateIngestionKeyLimitMutationOptions = <
Awaited<ReturnType<typeof createIngestionKeyLimit>>,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -467,12 +682,13 @@ export type CreateIngestionKeyLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof createIngestionKeyLimit>>
>;
export type CreateIngestionKeyLimitMutationBody =
| BodyType<GatewaytypesPostableIngestionKeyLimitDTO>
| BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>
| undefined;
export type CreateIngestionKeyLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create limit for the ingestion key
*/
export const useCreateIngestionKeyLimit = <
@@ -484,7 +700,7 @@ export const useCreateIngestionKeyLimit = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
>;
@@ -493,7 +709,7 @@ export const useCreateIngestionKeyLimit = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
> => {
@@ -501,6 +717,7 @@ export const useCreateIngestionKeyLimit = <
};
/**
* This endpoint deletes an ingestion key limit
* @deprecated
* @summary Delete limit for the ingestion key
*/
export const deleteIngestionKeyLimit = (
@@ -559,6 +776,7 @@ export type DeleteIngestionKeyLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete limit for the ingestion key
*/
export const useDeleteIngestionKeyLimit = <
@@ -581,6 +799,7 @@ export const useDeleteIngestionKeyLimit = <
};
/**
* This endpoint updates an ingestion key limit
* @deprecated
* @summary Update limit for the ingestion key
*/
export const updateIngestionKeyLimit = (
@@ -653,6 +872,7 @@ export type UpdateIngestionKeyLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Update limit for the ingestion key
*/
export const useUpdateIngestionKeyLimit = <
@@ -779,3 +999,370 @@ export const invalidateSearchIngestionKeys = async (
return queryClient;
};
/**
* This endpoint creates an ingestion limit for the ingestion key referenced by keyId
* @summary Create ingestion limit
*/
export const createIngestionLimit = (
gatewaytypesPostableIngestionKeyLimitDTO?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateIngestionLimit201>({
url: `/api/v2/gateway/ingestion_limits`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: gatewaytypesPostableIngestionKeyLimitDTO,
signal,
});
};
export const getCreateIngestionLimitMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
> => {
const mutationKey = ['createIngestionLimit'];
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 createIngestionLimit>>,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> }
> = (props) => {
const { data } = props ?? {};
return createIngestionLimit(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateIngestionLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof createIngestionLimit>>
>;
export type CreateIngestionLimitMutationBody =
| BodyType<GatewaytypesPostableIngestionKeyLimitDTO>
| undefined;
export type CreateIngestionLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create ingestion limit
*/
export const useCreateIngestionLimit = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
> => {
return useMutation(getCreateIngestionLimitMutationOptions(options));
};
/**
* This endpoint deletes an ingestion limit
* @summary Delete ingestion limit
*/
export const deleteIngestionLimit = (
{ limitId }: DeleteIngestionLimitPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/gateway/ingestion_limits/${limitId}`,
method: 'DELETE',
signal,
});
};
export const getDeleteIngestionLimitMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
> => {
const mutationKey = ['deleteIngestionLimit'];
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 deleteIngestionLimit>>,
{ pathParams: DeleteIngestionLimitPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteIngestionLimit(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteIngestionLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteIngestionLimit>>
>;
export type DeleteIngestionLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete ingestion limit
*/
export const useDeleteIngestionLimit = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
> => {
return useMutation(getDeleteIngestionLimitMutationOptions(options));
};
/**
* This endpoint returns an ingestion limit
* @summary Get ingestion limit
*/
export const getIngestionLimit = (
{ limitId }: GetIngestionLimitPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionLimit200>({
url: `/api/v2/gateway/ingestion_limits/${limitId}`,
method: 'GET',
signal,
});
};
export const getGetIngestionLimitQueryKey = ({
limitId,
}: GetIngestionLimitPathParameters) => {
return [`/api/v2/gateway/ingestion_limits/${limitId}`] as const;
};
export const getGetIngestionLimitQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionLimit>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ limitId }: GetIngestionLimitPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetIngestionLimitQueryKey({ limitId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getIngestionLimit>>
> = ({ signal }) => getIngestionLimit({ limitId }, signal);
return {
queryKey,
queryFn,
enabled: !!limitId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetIngestionLimitQueryResult = NonNullable<
Awaited<ReturnType<typeof getIngestionLimit>>
>;
export type GetIngestionLimitQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get ingestion limit
*/
export function useGetIngestionLimit<
TData = Awaited<ReturnType<typeof getIngestionLimit>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ limitId }: GetIngestionLimitPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionLimitQueryOptions({ limitId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get ingestion limit
*/
export const invalidateGetIngestionLimit = async (
queryClient: QueryClient,
{ limitId }: GetIngestionLimitPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionLimitQueryKey({ limitId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an ingestion limit
* @summary Update ingestion limit
*/
export const updateIngestionLimit = (
{ limitId }: UpdateIngestionLimitPathParameters,
gatewaytypesUpdatableIngestionKeyLimitDTO?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/gateway/ingestion_limits/${limitId}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: gatewaytypesUpdatableIngestionKeyLimitDTO,
signal,
});
};
export const getUpdateIngestionLimitMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
> => {
const mutationKey = ['updateIngestionLimit'];
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 updateIngestionLimit>>,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateIngestionLimit(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateIngestionLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof updateIngestionLimit>>
>;
export type UpdateIngestionLimitMutationBody =
| BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>
| undefined;
export type UpdateIngestionLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update ingestion limit
*/
export const useUpdateIngestionLimit = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
> => {
return useMutation(getUpdateIngestionLimitMutationOptions(options));
};

View File

@@ -0,0 +1,702 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
ActivateLicense201,
DeleteLicensePathParameters,
GetActiveLicense200,
GetLicense200,
GetLicensePathParameters,
LicensetypesPostableLicenseDTO,
ListLicenses200,
RefreshLicensePathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint validates the license key with the upstream server and activates the license for the organization.
* @deprecated
* @summary Activate a license.
*/
export const activateLicenseDeprecated = (
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v3/licenses`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: licensetypesPostableLicenseDTO,
signal,
});
};
export const getActivateLicenseDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
const mutationKey = ['activateLicenseDeprecated'];
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 activateLicenseDeprecated>>,
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
> = (props) => {
const { data } = props ?? {};
return activateLicenseDeprecated(data);
};
return { mutationFn, ...mutationOptions };
};
export type ActivateLicenseDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof activateLicenseDeprecated>>
>;
export type ActivateLicenseDeprecatedMutationBody =
| BodyType<LicensetypesPostableLicenseDTO>
| undefined;
export type ActivateLicenseDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Activate a license.
*/
export const useActivateLicenseDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
return useMutation(getActivateLicenseDeprecatedMutationOptions(options));
};
/**
* This endpoint refreshes the active license of the organization from the upstream server.
* @deprecated
* @summary Refresh a license.
*/
export const refreshLicenseDeprecated = (signal?: AbortSignal) => {
return GeneratedAPIInstance<void>({
url: `/api/v3/licenses`,
method: 'PUT',
signal,
});
};
export const getRefreshLicenseDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
> => {
const mutationKey = ['refreshLicenseDeprecated'];
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 refreshLicenseDeprecated>>,
void
> = () => {
return refreshLicenseDeprecated();
};
return { mutationFn, ...mutationOptions };
};
export type RefreshLicenseDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>
>;
export type RefreshLicenseDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Refresh a license.
*/
export const useRefreshLicenseDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
> => {
return useMutation(getRefreshLicenseDeprecatedMutationOptions(options));
};
/**
* This endpoint lists all the licenses of the organization.
* @summary List licenses.
*/
export const listLicenses = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListLicenses200>({
url: `/api/v4/licenses`,
method: 'GET',
signal,
});
};
export const getListLicensesQueryKey = () => {
return [`/api/v4/licenses`] as const;
};
export const getListLicensesQueryOptions = <
TData = Awaited<ReturnType<typeof listLicenses>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListLicensesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listLicenses>>> = ({
signal,
}) => listLicenses(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListLicensesQueryResult = NonNullable<
Awaited<ReturnType<typeof listLicenses>>
>;
export type ListLicensesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List licenses.
*/
export function useListLicenses<
TData = Awaited<ReturnType<typeof listLicenses>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListLicensesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List licenses.
*/
export const invalidateListLicenses = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListLicensesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint validates the license key with the upstream server and activates the license for the organization.
* @summary Activate a license.
*/
export const activateLicense = (
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ActivateLicense201>({
url: `/api/v4/licenses`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: licensetypesPostableLicenseDTO,
signal,
});
};
export const getActivateLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
const mutationKey = ['activateLicense'];
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 activateLicense>>,
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
> = (props) => {
const { data } = props ?? {};
return activateLicense(data);
};
return { mutationFn, ...mutationOptions };
};
export type ActivateLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof activateLicense>>
>;
export type ActivateLicenseMutationBody =
| BodyType<LicensetypesPostableLicenseDTO>
| undefined;
export type ActivateLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Activate a license.
*/
export const useActivateLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
return useMutation(getActivateLicenseMutationOptions(options));
};
/**
* This endpoint deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.
* @summary Delete a license.
*/
export const deleteLicense = (
{ id }: DeleteLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
> => {
const mutationKey = ['deleteLicense'];
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 deleteLicense>>,
{ pathParams: DeleteLicensePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteLicense(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteLicense>>
>;
export type DeleteLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete a license.
*/
export const useDeleteLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
> => {
return useMutation(getDeleteLicenseMutationOptions(options));
};
/**
* This endpoint gets the license by id.
* @summary Get a license.
*/
export const getLicense = (
{ id }: GetLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetLicense200>({
url: `/api/v4/licenses/${id}`,
method: 'GET',
signal,
});
};
export const getGetLicenseQueryKey = ({ id }: GetLicensePathParameters) => {
return [`/api/v4/licenses/${id}`] as const;
};
export const getGetLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLicensePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLicense>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetLicenseQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLicense>>> = ({
signal,
}) => getLicense({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getLicense>>, TError, TData> & {
queryKey: QueryKey;
};
};
export type GetLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getLicense>>
>;
export type GetLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a license.
*/
export function useGetLicense<
TData = Awaited<ReturnType<typeof getLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLicensePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLicense>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetLicenseQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a license.
*/
export const invalidateGetLicense = async (
queryClient: QueryClient,
{ id }: GetLicensePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetLicenseQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint refreshes the active license of the organization from the upstream server.
* @summary Refresh a license.
*/
export const refreshLicense = (
{ id }: RefreshLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses/${id}`,
method: 'PUT',
signal,
});
};
export const getRefreshLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
> => {
const mutationKey = ['refreshLicense'];
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 refreshLicense>>,
{ pathParams: RefreshLicensePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return refreshLicense(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RefreshLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshLicense>>
>;
export type RefreshLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Refresh a license.
*/
export const useRefreshLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
> => {
return useMutation(getRefreshLicenseMutationOptions(options));
};
/**
* This endpoint gets the active license of the organization.
* @summary Get the active license.
*/
export const getActiveLicense = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetActiveLicense200>({
url: `/api/v4/licenses/active`,
method: 'GET',
signal,
});
};
export const getGetActiveLicenseQueryKey = () => {
return [`/api/v4/licenses/active`] as const;
};
export const getGetActiveLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetActiveLicenseQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getActiveLicense>>> = ({
signal,
}) => getActiveLicense(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetActiveLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getActiveLicense>>
>;
export type GetActiveLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get the active license.
*/
export function useGetActiveLicense<
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetActiveLicenseQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get the active license.
*/
export const invalidateGetActiveLicense = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetActiveLicenseQueryKey() },
options,
);
return queryClient;
};

View File

@@ -0,0 +1,316 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
GetQuickFilters200,
GetQuickFiltersPathParameters,
ListQuickFilters200,
QuickfiltertypesUpdatableQuickFiltersDTO,
RenderErrorResponseDTO,
UpdateQuickFiltersPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns the org's quick filters for every source, each filter as a telemetry field key.
* @summary List quick filters
*/
export const listQuickFilters = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListQuickFilters200>({
url: `/api/v2/quick_filters`,
method: 'GET',
signal,
});
};
export const getListQuickFiltersQueryKey = () => {
return [`/api/v2/quick_filters`] as const;
};
export const getListQuickFiltersQueryOptions = <
TData = Awaited<ReturnType<typeof listQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListQuickFiltersQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listQuickFilters>>> = ({
signal,
}) => listQuickFilters(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListQuickFiltersQueryResult = NonNullable<
Awaited<ReturnType<typeof listQuickFilters>>
>;
export type ListQuickFiltersQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List quick filters
*/
export function useListQuickFilters<
TData = Awaited<ReturnType<typeof listQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListQuickFiltersQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List quick filters
*/
export const invalidateListQuickFilters = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListQuickFiltersQueryKey() },
options,
);
return queryClient;
};
/**
* Returns the org's quick filters for one source, each filter as a telemetry field key.
* @summary Get a source's quick filters
*/
export const getQuickFilters = (
{ source }: GetQuickFiltersPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetQuickFilters200>({
url: `/api/v2/quick_filters/${source}`,
method: 'GET',
signal,
});
};
export const getGetQuickFiltersQueryKey = ({
source,
}: GetQuickFiltersPathParameters) => {
return [`/api/v2/quick_filters/${source}`] as const;
};
export const getGetQuickFiltersQueryOptions = <
TData = Awaited<ReturnType<typeof getQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetQuickFiltersPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetQuickFiltersQueryKey({ source });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getQuickFilters>>> = ({
signal,
}) => getQuickFilters({ source }, signal);
return {
queryKey,
queryFn,
enabled: !!source,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetQuickFiltersQueryResult = NonNullable<
Awaited<ReturnType<typeof getQuickFilters>>
>;
export type GetQuickFiltersQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a source's quick filters
*/
export function useGetQuickFilters<
TData = Awaited<ReturnType<typeof getQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetQuickFiltersPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetQuickFiltersQueryOptions({ source }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a source's quick filters
*/
export const invalidateGetQuickFilters = async (
queryClient: QueryClient,
{ source }: GetQuickFiltersPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetQuickFiltersQueryKey({ source }) },
options,
);
return queryClient;
};
/**
* Replaces the org's quick filters for the source named in the path.
* @summary Update quick filters
*/
export const updateQuickFilters = (
{ source }: UpdateQuickFiltersPathParameters,
quickfiltertypesUpdatableQuickFiltersDTO?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/quick_filters/${source}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: quickfiltertypesUpdatableQuickFiltersDTO,
signal,
});
};
export const getUpdateQuickFiltersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
> => {
const mutationKey = ['updateQuickFilters'];
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 updateQuickFilters>>,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateQuickFilters(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateQuickFiltersMutationResult = NonNullable<
Awaited<ReturnType<typeof updateQuickFilters>>
>;
export type UpdateQuickFiltersMutationBody =
| BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>
| undefined;
export type UpdateQuickFiltersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update quick filters
*/
export const useUpdateQuickFilters = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
> => {
return useMutation(getUpdateQuickFiltersMutationOptions(options));
};

View File

@@ -385,6 +385,38 @@ export interface AlertmanagertypesGoogleChatReceiverConfigDTO {
webhook_url?: ConfigSecretURLDTO;
}
export type AlertmanagertypesIncidentIOReceiverConfigDTOMetadata = {
[key: string]: string;
};
export interface AlertmanagertypesIncidentIOReceiverConfigDTO {
/**
* @type string
*/
description?: string;
http_config?: ConfigHTTPClientConfigDTO;
/**
* @type object
*/
metadata?: AlertmanagertypesIncidentIOReceiverConfigDTOMetadata;
/**
* @type boolean
*/
send_resolved?: boolean;
/**
* @type string
*/
title?: string;
/**
* @type string
*/
token?: string;
/**
* @type string
*/
url?: string;
}
export interface AlertmanagertypesJSMOpsReceiverConfigDTO {
/**
* @type string
@@ -685,39 +717,6 @@ export interface ConfigEmailConfigDTO {
to?: string;
}
export type TimeDurationDTO = number;
export interface ConfigURLType2DTO {
[key: string]: unknown;
}
export interface ConfigIncidentioConfigDTO {
/**
* @type string
*/
alert_source_token?: string;
/**
* @type string
*/
alert_source_token_file?: string;
http_config?: ConfigHTTPClientConfigDTO;
/**
* @type integer
* @minimum 0
*/
max_alerts?: number;
/**
* @type boolean
*/
send_resolved?: boolean;
timeout?: TimeDurationDTO;
url?: ConfigURLType2DTO;
/**
* @type string
*/
url_file?: string;
}
export interface ConfigMattermostFieldDTO {
/**
* @type boolean,null
@@ -903,6 +902,10 @@ export interface ConfigMSTeamsV2ConfigDTO {
webhook_url_file?: string;
}
export interface ConfigURLType2DTO {
[key: string]: unknown;
}
export interface ConfigOpsGenieConfigResponderDTO {
/**
* @type string
@@ -1011,6 +1014,8 @@ export interface ConfigPagerdutyLinkDTO {
text?: string;
}
export type TimeDurationDTO = number;
export type ConfigPagerdutyConfigDTODetails = { [key: string]: unknown };
export interface ConfigPagerdutyConfigDTO {
@@ -1672,7 +1677,7 @@ export type AlertmanagertypesPostableChannelDTO = unknown & {
/**
* @type array
*/
incidentio_configs?: ConfigIncidentioConfigDTO[];
incidentio_configs?: AlertmanagertypesIncidentIOReceiverConfigDTO[];
/**
* @type array
*/
@@ -1803,7 +1808,7 @@ export interface AlertmanagertypesReceiverDTO {
/**
* @type array
*/
incidentio_configs?: ConfigIncidentioConfigDTO[];
incidentio_configs?: AlertmanagertypesIncidentIOReceiverConfigDTO[];
/**
* @type array
*/
@@ -3300,6 +3305,33 @@ export interface CommonJSONRefDTO {
$ref?: string;
}
export interface ConfigIncidentioConfigDTO {
/**
* @type string
*/
alert_source_token?: string;
/**
* @type string
*/
alert_source_token_file?: string;
http_config?: ConfigHTTPClientConfigDTO;
/**
* @type integer
* @minimum 0
*/
max_alerts?: number;
/**
* @type boolean
*/
send_resolved?: boolean;
timeout?: TimeDurationDTO;
url?: ConfigURLType2DTO;
/**
* @type string
*/
url_file?: string;
}
export type ConfigJiraConfigDTOCustomFields = { [key: string]: unknown };
export interface ConfigJiraFieldConfigDTO {
@@ -5461,6 +5493,34 @@ export interface FeaturetypesGettableFeatureDTO {
variants?: FeaturetypesGettableFeatureDTOVariants;
}
export interface GatewaytypesLimitValueDTO {
/**
* @type integer,null
*/
count?: number | null;
/**
* @type integer,null
*/
size?: number | null;
}
export interface GatewaytypesLimitConfigDTO {
day?: GatewaytypesLimitValueDTO;
second?: GatewaytypesLimitValueDTO;
}
export interface GatewaytypesDeprecatedPostableIngestionKeyLimitDTO {
config?: GatewaytypesLimitConfigDTO;
/**
* @type string
*/
signal?: string;
/**
* @type array,null
*/
tags?: string[] | null;
}
export interface GatewaytypesGettableCreatedIngestionKeyDTO {
/**
* @type string
@@ -5498,22 +5558,6 @@ export interface GatewaytypesPaginationDTO {
total?: number;
}
export interface GatewaytypesLimitValueDTO {
/**
* @type integer,null
*/
count?: number | null;
/**
* @type integer,null
*/
size?: number | null;
}
export interface GatewaytypesLimitConfigDTO {
day?: GatewaytypesLimitValueDTO;
second?: GatewaytypesLimitValueDTO;
}
export interface GatewaytypesLimitMetricValueDTO {
/**
* @type integer
@@ -5631,6 +5675,10 @@ export interface GatewaytypesPostableIngestionKeyDTO {
export interface GatewaytypesPostableIngestionKeyLimitDTO {
config?: GatewaytypesLimitConfigDTO;
/**
* @type string
*/
keyId: string;
/**
* @type string
*/
@@ -7313,6 +7361,249 @@ export interface InframonitoringtypesVolumesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface LicensetypesFeatureDTO {
/**
* @type boolean
*/
active?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
route?: string;
/**
* @type integer
* @format int64
*/
usage?: number;
/**
* @type integer
* @format int64
*/
usage_limit?: number;
}
export interface LicensetypesLicenseEventQueueDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
event: string;
/**
* @type string
* @format date-time
*/
scheduledAt: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface LicensetypesLicensePlanDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type boolean
*/
isActive: boolean;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface LicensetypesGettableActiveLicenseDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
eventQueue: LicensetypesLicenseEventQueueDTO;
/**
* @type array
*/
features: LicensetypesFeatureDTO[];
/**
* @type string
* @format date-time
*/
freeUntil: string;
/**
* @type string
*/
id: string;
plan: LicensetypesLicensePlanDTO;
/**
* @type string
*/
platform: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type integer
* @format int64
*/
validFrom: number;
/**
* @type integer
* @format int64
*/
validUntil: number;
}
export interface LicensetypesGettableLicenseDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
eventQueue: LicensetypesLicenseEventQueueDTO;
/**
* @type array
*/
features: LicensetypesFeatureDTO[];
/**
* @type string
* @format date-time
*/
freeUntil: string;
/**
* @type string
*/
id: string;
plan: LicensetypesLicensePlanDTO;
/**
* @type string
*/
platform: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type integer
* @format int64
*/
validFrom: number;
/**
* @type integer
* @format int64
*/
validUntil: number;
}
export interface LicensetypesGettableLicenseWithKeyDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
eventQueue: LicensetypesLicenseEventQueueDTO;
/**
* @type array
*/
features: LicensetypesFeatureDTO[];
/**
* @type string
* @format date-time
*/
freeUntil: string;
/**
* @type string
*/
id: string;
/**
* @type string
* @format password
*/
key: string;
plan: LicensetypesLicensePlanDTO;
/**
* @type string
*/
platform: string;
/**
* @type string
*/
state: string;
/**
* @type string
*/
status: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type integer
* @format int64
*/
validFrom: number;
/**
* @type integer
* @format int64
*/
validUntil: number;
}
export interface LicensetypesPostableLicenseDTO {
/**
* @type string
* @format password
*/
key?: string;
}
/**
* @nullable
*/
@@ -8735,6 +9026,47 @@ export enum Querybuildertypesv5QueryTypeDTO {
clickhouse_sql = 'clickhouse_sql',
promql = 'promql',
}
export enum QuickfiltertypesSourceDTO {
traces = 'traces',
logs = 'logs',
api_monitoring = 'api_monitoring',
exceptions = 'exceptions',
meter = 'meter',
ai_observability = 'ai_observability',
}
export interface QuickfiltertypesSourceFiltersDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type array
*/
filters: TelemetrytypesTelemetryFieldKeyDTO[];
/**
* @type string
*/
id: string;
/**
* @type string
*/
orgId: string;
source: QuickfiltertypesSourceDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface QuickfiltertypesUpdatableQuickFiltersDTO {
/**
* @type array
*/
filters: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface RenderErrorResponseDTO {
error: ErrorsJSONDTO;
/**
@@ -10547,6 +10879,11 @@ export type GetAIObservabilityFieldsValuesParams = {
* @description undefined
*/
name?: string;
/**
* @type string
* @description undefined
*/
existingQuery?: string;
};
export type GetAIObservabilityFieldsValues200 = {
@@ -11665,9 +12002,34 @@ export type CreateIngestionKey201 = {
export type DeleteIngestionKeyPathParameters = {
keyId: string;
};
export type GetIngestionKeyPathParameters = {
keyId: string;
};
export type GetIngestionKey200 = {
data: GatewaytypesIngestionKeyDTO;
/**
* @type string
*/
status: string;
};
export type UpdateIngestionKeyPathParameters = {
keyId: string;
};
export type GetIngestionKeyLimitsPathParameters = {
keyId: string;
};
export type GetIngestionKeyLimits200 = {
/**
* @type array,null
*/
data: GatewaytypesLimitDTO[] | null;
/**
* @type string
*/
status: string;
};
export type CreateIngestionKeyLimitPathParameters = {
keyId: string;
};
@@ -11711,6 +12073,31 @@ export type SearchIngestionKeys200 = {
status: string;
};
export type CreateIngestionLimit201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteIngestionLimitPathParameters = {
limitId: string;
};
export type GetIngestionLimitPathParameters = {
limitId: string;
};
export type GetIngestionLimit200 = {
data: GatewaytypesLimitDTO;
/**
* @type string
*/
status: string;
};
export type UpdateIngestionLimitPathParameters = {
limitId: string;
};
export type Healthz200 = {
data: FactoryResponseDTO;
/**
@@ -12136,6 +12523,31 @@ export type GetPublicDashboardPanelQueryRangeV2200 = {
status: string;
};
export type ListQuickFilters200 = {
/**
* @type array
*/
data: QuickfiltertypesSourceFiltersDTO[];
/**
* @type string
*/
status: string;
};
export type GetQuickFiltersPathParameters = {
source: string;
};
export type GetQuickFilters200 = {
data: QuickfiltertypesSourceFiltersDTO;
/**
* @type string
*/
status: string;
};
export type UpdateQuickFiltersPathParameters = {
source: string;
};
export type Readyz200 = {
data: FactoryResponseDTO;
/**
@@ -12738,6 +13150,50 @@ export type GetFlamegraph200 = {
status: string;
};
export type ListLicenses200 = {
/**
* @type array
*/
data: LicensetypesGettableLicenseDTO[];
/**
* @type string
*/
status: string;
};
export type ActivateLicense201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteLicensePathParameters = {
id: string;
};
export type GetLicensePathParameters = {
id: string;
};
export type GetLicense200 = {
data: LicensetypesGettableLicenseWithKeyDTO;
/**
* @type string
*/
status: string;
};
export type RefreshLicensePathParameters = {
id: string;
};
export type GetActiveLicense200 = {
data: LicensetypesGettableActiveLicenseDTO;
/**
* @type string
*/
status: string;
};
export type GetWaterfallV4PathParameters = {
traceID: string;
};

View File

@@ -1,25 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/quickFilters/getCustomFilters';
const getCustomFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const { signal } = props;
try {
const response = await axios.get(`/orgs/me/filters/${signal}`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getCustomFilters;

View File

@@ -1,13 +0,0 @@
import axios from 'api';
import { AxiosError } from 'axios';
import { SuccessResponse } from 'types/api';
import { UpdateCustomFiltersProps } from 'types/api/quickFilters/updateCustomFilters';
const updateCustomFiltersAPI = async (
props: UpdateCustomFiltersProps,
): Promise<SuccessResponse<void> | AxiosError> =>
axios.put(`/orgs/me/filters`, {
...props.data,
});
export default updateCustomFiltersAPI;

View File

@@ -1,25 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
LicenseEventQueueResModel,
PayloadProps,
} from 'types/api/licensesV3/getActive';
const getActive = async (): Promise<
SuccessResponseV2<LicenseEventQueueResModel>
> => {
try {
const response = await axios.get<PayloadProps>('/licenses/active');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getActive;

View File

@@ -1,24 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/licenses', {
key: props.key,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@@ -1,20 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/licenses/apply';
const apply = async (): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.put<PayloadProps>('/licenses');
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@@ -57,8 +57,8 @@ function MenuItemGenerator({
handleExplorerTabChange(currentPanelType, {
query,
name,
id,
viewName: name,
viewKey: id,
});
},
[viewData, handleExplorerTabChange],

View File

@@ -3,6 +3,7 @@ import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { LoaderCircle } from '@signozhq/icons';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
@@ -74,6 +75,10 @@ export default function CheckboxFilterV2(
searchText,
existingQuery,
metricNamespace: useFieldApis.metricNamespace,
source:
source === QuickFiltersSource.METER_EXPLORER
? TelemetrytypesSourceDTO.meter
: undefined,
startUnixMilli: useFieldApis.startUnixMilli,
endUnixMilli: useFieldApis.endUnixMilli,
enabled: isOpen,

View File

@@ -1,7 +1,11 @@
import { useMemo } from 'react';
import { useGetFieldsValues } from 'api/generated/services/fields';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import {
TelemetrytypesSignalDTO,
TelemetrytypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
@@ -10,6 +14,7 @@ interface UseFieldValuesProps {
searchText: string;
existingQuery?: string;
metricNamespace?: string;
source?: TelemetrytypesSourceDTO;
startUnixMilli?: number;
endUnixMilli?: number;
enabled: boolean;
@@ -33,6 +38,7 @@ export function useFieldValues({
searchText,
existingQuery,
metricNamespace,
source,
startUnixMilli,
endUnixMilli,
enabled,
@@ -46,6 +52,7 @@ export function useFieldValues({
searchText,
existingQuery,
metricNamespace,
source,
startUnixMilli,
// This field does not affect the backend but I wanted to keep it here
// in case we add the support in the future
@@ -75,6 +82,12 @@ export function useFieldValues({
}, [data]);
const allValues: string[] = useMemo(() => {
// Bool fields should always offer true/false.
// The values api returns nothing for them.
if (filter.attributeKey.dataType === DataTypes.bool) {
return ['true', 'false'];
}
const values = data?.data?.values;
if (!values) {
return [];
@@ -91,7 +104,7 @@ export function useFieldValues({
.map((value) => value.toString()) || [];
return [...stringValues, ...numberValues];
}, [data]);
}, [data, filter.attributeKey.dataType]);
return { relatedValues, allValues, isLoading, isFetching };
}

View File

@@ -17,7 +17,7 @@ import { CSS } from '@dnd-kit/utilities';
import { Button } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
function SortableFilter({
filter,
@@ -25,13 +25,13 @@ function SortableFilter({
allowDrag,
allowRemove,
}: {
filter: FilterType;
onRemove: (filter: FilterType) => void;
filter: TelemetryFieldKey;
onRemove: (filter: TelemetryFieldKey) => void;
allowDrag: boolean;
allowRemove: boolean;
}): JSX.Element {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: filter.key });
useSortable({ id: filter.key as string });
const style = {
transform: CSS.Transform.toString(transform),
@@ -46,14 +46,14 @@ function SortableFilter({
>
<div {...attributes} {...listeners} className="drag-handle">
{allowDrag && <GripVertical size={16} />}
{filter.key}
{filter.name}
</div>
{allowRemove && (
<Button
className="remove-filter-btn periscope-btn"
size="small"
onClick={(): void => {
onRemove(filter as FilterType);
onRemove(filter);
}}
>
Remove
@@ -69,8 +69,8 @@ function AddedFilters({
setAddedFilters,
}: {
inputValue: string;
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const sensors = useSensors(useSensor(PointerSensor));
@@ -90,12 +90,12 @@ function AddedFilters({
const filteredAddedFilters = useMemo(
() =>
addedFilters.filter((filter) =>
filter.key.toLowerCase().includes(inputValue.toLowerCase()),
filter.name.toLowerCase().includes(inputValue.toLowerCase()),
),
[addedFilters, inputValue],
);
const handleRemoveFilter = (filter: FilterType): void => {
const handleRemoveFilter = (filter: TelemetryFieldKey): void => {
setAddedFilters((prev) => prev.filter((f) => f.key !== filter.key));
};
@@ -116,7 +116,7 @@ function AddedFilters({
<div className="no-values-found">No values found</div>
) : (
<SortableContext
items={addedFilters.map((f) => f.key)}
items={addedFilters.map((f) => f.key as string)}
strategy={verticalListSortingStrategy}
disabled={!allowDrag}
>

View File

@@ -4,14 +4,9 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
import { FieldContext, TelemetryFieldKey } from 'types/api/v5/queryRange';
function OtherFiltersSkeleton(): JSX.Element {
return (
@@ -37,106 +32,49 @@ function OtherFilters({
}: {
signal: SignalType | undefined;
inputValue: string;
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const isLogDataSource = useMemo(
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
[signal],
);
const isMeterDataSource = useMemo(
() => signal && signal === SignalType.METER_EXPLORER,
[signal],
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
const { data, isFetching } = useGetQueryKeySuggestions(
{
searchText: inputValue,
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
signalSource: isMeterDataSource ? 'meter' : '',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, signal, inputValue],
enabled: !!signal,
},
);
const { data: suggestionsData, isFetching: isFetchingSuggestions } =
useGetAttributeSuggestions(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
filters: {} as TagFilter,
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isLogDataSource,
},
);
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
const rawSuggestions = Object.values(data?.data?.data?.keys || {}).flat();
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
// add, render) can trust it.
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
name: attr.name,
signal: attr.signal,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType,
key: buildCompositeKey(attr.name, attr.fieldContext, attr.fieldDataType),
}));
const { data: aggregateKeysData, isFetching: isFetchingAggregateKeys } =
useGetAggregateKeys(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
aggregateOperator: 'noop',
aggregateAttribute: '',
tagType: '',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
},
const addedKeys = new Set(
addedFilters.map((filter) =>
buildCompositeKey(filter.name, filter.fieldContext, filter.fieldDataType),
),
);
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
}, [data, addedFilters]);
const { data: fieldKeysData, isLoading: isLoadingFieldKeys } =
useGetQueryKeySuggestions(
{
searchText: inputValue,
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
signalSource: 'meter',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isMeterDataSource,
},
);
const otherFilters = useMemo(() => {
let filterAttributes;
if (isLogDataSource) {
filterAttributes = suggestionsData?.payload?.attributes || [];
} else if (isMeterDataSource) {
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
fieldKeysData?.data?.data?.keys || {},
)?.flat();
filterAttributes = fieldKeys.map(
(attr) =>
({
key: attr.name,
dataType: attr.fieldDataType,
type: attr.fieldContext,
signal: attr.signal,
}) as BaseAutocompleteData,
);
} else {
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
}
return filterAttributes?.filter(
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
);
}, [
suggestionsData,
aggregateKeysData,
addedFilters,
isLogDataSource,
fieldKeysData,
isMeterDataSource,
]);
const handleAddFilter = (filter: FilterType): void => {
setAddedFilters((prev) => [
...prev,
{
key: filter.key,
dataType: filter.dataType,
type: filter.type,
},
]);
const handleAddFilter = (filter: TelemetryFieldKey): void => {
setAddedFilters((prev) => [...prev, filter]);
};
const renderFilters = (): React.ReactNode => {
const isLoading =
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
if (isLoading) {
if (isFetching) {
return <OtherFiltersSkeleton />;
}
if (!otherFilters?.length) {
@@ -145,11 +83,11 @@ function OtherFilters({
return otherFilters.map((filter) => (
<div key={filter.key} className="qf-filter-item other-filters-item">
<div className="qf-filter-key">{filter.key}</div>
<div className="qf-filter-key">{filter.name}</div>
<Button
className="add-filter-btn periscope-btn"
size="small"
onClick={(): void => handleAddFilter(filter as FilterType)}
onClick={(): void => handleAddFilter(filter)}
>
Add
</Button>

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { SignalType } from '../types';
import AddedFilters from './AddedFilters';
@@ -19,7 +18,7 @@ function QuickFiltersSettings({
}: {
signal: SignalType | undefined;
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
customFilters: FilterType[];
customFilters: TelemetryFieldKey[];
refetchCustomFilters: () => void;
}): JSX.Element {
const {
@@ -28,6 +27,7 @@ function QuickFiltersSettings({
addedFilters,
setAddedFilters,
handleSaveChanges,
hasUnsavedChanges,
isUpdatingCustomFilters,
inputValue,
handleInputChange,
@@ -39,18 +39,6 @@ function QuickFiltersSettings({
signal,
});
const hasUnsavedChanges = useMemo(
() =>
// check if both arrays have the same length and same order of elements
!(
addedFilters.length === customFilters.length &&
addedFilters.every(
(filter, index) => filter.key === customFilters[index].key,
)
),
[addedFilters, customFilters],
);
return (
<>
<div className="qf-header">

View File

@@ -1,27 +1,31 @@
import { useCallback, useState } from 'react';
import { useMutation } from 'react-query';
import { useCallback, useMemo, useState } from 'react';
import { useUpdateQuickFilters } from 'api/generated/services/quick-filter';
import logEvent from 'api/common/logEvent';
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
import axios, { AxiosError } from 'axios';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesFieldDataTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SignalType } from 'components/QuickFilters/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
interface UseQuickFilterSettingsProps {
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
customFilters: FilterType[];
customFilters: TelemetryFieldKey[];
refetchCustomFilters: () => void;
signal?: SignalType;
}
interface UseQuickFilterSettingsReturn {
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
handleSettingsClose: () => void;
handleDiscardChanges: () => void;
handleSaveChanges: () => void;
hasUnsavedChanges: boolean;
isUpdatingCustomFilters: boolean;
inputValue: string;
setInputValue: React.Dispatch<React.SetStateAction<string>>;
@@ -37,27 +41,43 @@ const useQuickFilterSettings = ({
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
const [inputValue, setInputValue] = useState<string>('');
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
const [addedFilters, setAddedFilters] = useState<FilterType[]>(customFilters);
const normalizedCustomFilters = useMemo<TelemetryFieldKey[]>(
() =>
customFilters.map((filter) => ({
...filter,
key: buildCompositeKey(
filter.name,
filter.fieldContext,
filter.fieldDataType,
),
})),
[customFilters],
);
const [addedFilters, setAddedFilters] = useState<TelemetryFieldKey[]>(
normalizedCustomFilters,
);
const { notifications } = useNotifications();
const { mutate: updateCustomFilters, isLoading: isUpdatingCustomFilters } =
useMutation(updateCustomFiltersAPI, {
onSuccess: () => {
setIsSettingsOpen(false);
refetchCustomFilters();
logEvent('Quick Filters Settings: changes saved', {
addedFilters,
});
notifications.success({
message: 'Quick filters updated successfully',
placement: 'bottomRight',
});
},
onError: (error: AxiosError) => {
notifications.error({
message: axios.isAxiosError(error) ? error.message : SOMETHING_WENT_WRONG,
placement: 'bottomRight',
});
useUpdateQuickFilters({
mutation: {
onSuccess: () => {
setIsSettingsOpen(false);
refetchCustomFilters();
void logEvent('Quick Filters Settings: changes saved', {
addedFilters,
});
notifications.success({
message: 'Quick filters updated successfully',
placement: 'bottomRight',
});
},
onError: (error) => {
notifications.error({
message: error.message || SOMETHING_WENT_WRONG,
placement: 'bottomRight',
});
},
},
});
const debouncedUpdate = useDebouncedFn((value) => {
@@ -78,17 +98,30 @@ const useQuickFilterSettings = ({
}, [setIsSettingsOpen]);
const handleDiscardChanges = useCallback((): void => {
setAddedFilters(customFilters);
}, [customFilters, setAddedFilters]);
setAddedFilters(normalizedCustomFilters);
}, [normalizedCustomFilters, setAddedFilters]);
const hasUnsavedChanges = useMemo(
() =>
!(
addedFilters.length === normalizedCustomFilters.length &&
addedFilters.every(
(filter, index) => filter.key === normalizedCustomFilters[index].key,
)
),
[addedFilters, normalizedCustomFilters],
);
const handleSaveChanges = useCallback((): void => {
if (signal) {
updateCustomFilters({
data: {
// Send only the stored TelemetryFieldKey fields; the composite `key`
// is UI-only.
filters: addedFilters.map((filter) => ({
key: filter.key,
datatype: filter.dataType,
type: filter.type,
name: filter.name,
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
})),
signal,
},
@@ -102,6 +135,7 @@ const useQuickFilterSettings = ({
addedFilters,
setAddedFilters,
handleSaveChanges,
hasUnsavedChanges,
isUpdatingCustomFilters,
inputValue,
setInputValue,

View File

@@ -1,8 +1,6 @@
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import getCustomFilters from 'api/quickFilters/getCustomFilters';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { IQuickFiltersConfig, SignalType } from '../types';
import { getFilterConfig } from '../utils';
@@ -13,7 +11,7 @@ interface UseFilterConfigProps {
}
interface UseFilterConfigReturn {
filterConfig: IQuickFiltersConfig[];
customFilters: FilterType[];
customFilters: TelemetryFieldKey[];
isCustomFiltersLoading: boolean;
isDynamicFilters: boolean;
refetchCustomFilters: () => void;
@@ -25,17 +23,16 @@ const useFilterConfig = ({
}: UseFilterConfigProps): UseFilterConfigReturn => {
const {
isFetching: isCustomFiltersLoading,
data: customFilters = [],
data,
refetch,
} = useQuery<FilterType[], Error>(
[REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal],
async () => {
const res = await getCustomFilters({ signal: signal || '' });
return 'payload' in res && res.payload?.filters ? res.payload.filters : [];
},
{
enabled: !!signal,
},
} = useGetQuickFilters(
{ signalName: signal ?? '' },
{ query: { enabled: !!signal } },
);
const customFilters = useMemo<TelemetryFieldKey[]>(
() => (data?.data?.filters ?? []) as TelemetryFieldKey[],
[data],
);
const isDynamicFilters = useMemo(

View File

@@ -0,0 +1,24 @@
import { useMemo } from 'react';
import {
NANO_SECOND_MULTIPLIER,
useLastComputedMinMax,
} from 'store/globalTime';
import { QuickFilterCheckboxUseFieldApis } from '../types';
/**
* Builds the `useFieldApis` config for a signal quick-filter page.
* if existingQuery is sent null, related values are not fetched
*/
export function useSignalFieldApis(): QuickFilterCheckboxUseFieldApis {
const { minTime, maxTime } = useLastComputedMinMax();
return useMemo(
() => ({
startUnixMilli: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
endUnixMilli: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
existingQuery: null,
}),
[minTime, maxTime],
);
}

View File

@@ -11,7 +11,7 @@ import {
} from 'mocks-server/__mockdata__/customQuickFilters';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import '@testing-library/jest-dom';
@@ -34,9 +34,9 @@ const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const BASE_URL = ENVIRONMENT.baseURL;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
@@ -338,6 +338,63 @@ describe('Quick Filters with custom filters', () => {
);
});
it('keeps same-name fields with different context as distinct entries', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get(quickFiltersSuggestionsURL, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
complete: true,
keys: {
level: [
{
name: 'level',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
{
name: 'level',
fieldContext: 'span',
fieldDataType: 'string',
signal: 'logs',
},
],
},
},
}),
),
),
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!;
// Both `level` variants are shown despite sharing a name.
await waitFor(() =>
expect(within(otherSection).getAllByText('level')).toHaveLength(2),
);
// Adding one variant removes only that one; the other stays.
const firstLevel = within(otherSection).getAllByText('level')[0];
const addButton = firstLevel.parentElement?.querySelector('button');
await user.click(addButton as HTMLButtonElement);
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
await waitFor(() => {
expect(within(addedSection).getAllByText('level')).toHaveLength(1);
expect(within(otherSection).getAllByText('level')).toHaveLength(1);
});
});
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -458,7 +515,7 @@ describe('Quick Filters with custom filters', () => {
const requestBody = putHandler.mock.calls[0][0];
expect(requestBody.filters).toStrictEqual(
expect.arrayContaining([
expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }),
expect.not.objectContaining({ name: FILTER_OS_DESCRIPTION }),
]),
);
expect(requestBody.signal).toBe(SIGNAL);
@@ -612,9 +669,9 @@ describe('Quick Filters refetch behavior', () => {
filters: [
...(quickFiltersListResponse.data.filters ?? []),
{
key: 'new.custom.filter',
dataType: 'string',
type: 'resource',
name: 'new.custom.filter',
fieldDataType: 'string',
fieldContext: 'resource',
} as const,
],
},

View File

@@ -1,5 +1,10 @@
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesFieldDataTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
@@ -12,6 +17,39 @@ const FILTER_TYPE_MAP: Record<string, FiltersType> = {
duration_nano: FiltersType.DURATION,
};
// Both maps below (and mapFieldDataType/mapFieldContext) exist only for the old
// v3 attribute-values fetch in useCheckboxFilterValues, which is the sole reader
// of attributeKey.dataType/type. Query/list endpoints don't consume them (v5 and
// the API-monitoring/exceptions/infra paths all send a name-based expression).
// Once the values fetch moves to fields/values (by name) in Phase A, this whole
// mapping can be removed and attributeKey reduced to { id, key }.
// The new field data types are rendered down to the v3 spellings the
// attribute-values call expects, matching the backend's legacy conversion
// (number -> float64).
const FIELD_DATA_TYPE_TO_DATA_TYPE: Record<string, DataTypes> = {
[TelemetrytypesFieldDataTypeDTO.string]: DataTypes.String,
[TelemetrytypesFieldDataTypeDTO.bool]: DataTypes.bool,
[TelemetrytypesFieldDataTypeDTO.float64]: DataTypes.Float64,
[TelemetrytypesFieldDataTypeDTO.int64]: DataTypes.Int64,
[TelemetrytypesFieldDataTypeDTO.number]: DataTypes.Float64,
};
// Only tag and resource exist in the v3 attribute-type enum; other contexts
// render as empty so the still-live v3 values path never sees a spelling it
// can't use, matching the backend's legacy conversion.
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Record<string, string> = {
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
[TelemetrytypesFieldContextDTO.resource]: 'resource',
};
const mapFieldDataType = (fieldDataType?: string): DataTypes =>
(fieldDataType && FIELD_DATA_TYPE_TO_DATA_TYPE[fieldDataType]) ||
DataTypes.EMPTY;
const mapFieldContext = (fieldContext?: string): string =>
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
const getFilterName = (str: string): string => {
if (FILTER_TITLE_MAP[str]) {
return FILTER_TITLE_MAP[str];
@@ -26,16 +64,16 @@ const getFilterName = (str: string): string => {
.join(' ');
};
const getFilterType = (att: FilterType): FiltersType => {
if (FILTER_TYPE_MAP[att.key]) {
return FILTER_TYPE_MAP[att.key];
const getFilterType = (att: TelemetryFieldKey): FiltersType => {
if (FILTER_TYPE_MAP[att.name]) {
return FILTER_TYPE_MAP[att.name];
}
return FiltersType.CHECKBOX;
};
export const getFilterConfig = (
signal?: SignalType,
customFilters?: FilterType[],
customFilters?: TelemetryFieldKey[],
config?: IQuickFiltersConfig[],
): IQuickFiltersConfig[] => {
if (!customFilters?.length || !signal) {
@@ -46,13 +84,13 @@ export const getFilterConfig = (
(att, index) =>
({
type: getFilterType(att),
title: getFilterName(att.key),
title: getFilterName(att.name),
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
attributeKey: {
id: att.key,
key: att.key,
dataType: att.dataType,
type: att.type,
id: att.name,
key: att.name,
dataType: mapFieldDataType(att.fieldDataType),
type: mapFieldContext(att.fieldContext),
},
defaultOpen: index < 2,
}) as IQuickFiltersConfig,

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import refreshPaymentStatus from 'api/v3/licenses/put';
import { refreshLicense } from 'api/generated/services/licenses';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { RefreshCcw } from '@signozhq/icons';
@@ -14,17 +14,21 @@ function RefreshPaymentStatus({
className?: string;
}): JSX.Element {
const { t } = useTranslation(['failedPayment']);
const { activeLicenseRefetch } = useAppContext();
const { activeLicense, activeLicenseRefetch } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const handleRefreshPaymentStatus = async (): Promise<void> => {
if (!activeLicense) {
return;
}
setIsLoading(true);
try {
await refreshPaymentStatus();
await refreshLicense({ id: activeLicense.id });
await Promise.all([activeLicenseRefetch()]);
activeLicenseRefetch();
} catch (e) {
console.error(e);
}

View File

@@ -28,7 +28,6 @@ export const REACT_QUERY_KEY = {
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
GET_TRACE_V4_WATERFALL: 'GET_TRACE_V4_WATERFALL',
GET_TRACE_AGGREGATIONS: 'GET_TRACE_AGGREGATIONS',

View File

@@ -2,6 +2,7 @@ import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import {
GoogleChatInitialConfig,
IncidentIOInitialConfig,
JiraInitialConfig,
JsmOpsInitialConfig,
} from 'container/CreateAlertChannels/defaults';
@@ -585,7 +586,7 @@ describe('Create Alert Channel', () => {
description: 'jira_site_invalid',
}),
);
});
}, 15000);
it('Should send a jira_configs payload with basic auth', async () => {
let requestBody: unknown;
@@ -737,6 +738,122 @@ describe('Create Alert Channel', () => {
});
});
});
describe('incident.io', () => {
const incidentIOURL =
'https://api.incident.io/v2/alert_events/http/01M0D1JNVBGBGVTWX053EM12XV';
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.IncidentIO} />);
});
it('Should display the URL and token fields with the docs tip', () => {
testLabelInputAndHelpValue({
labelText: 'field_incidentio_url',
testId: 'incidentio-url-textbox',
});
testLabelInputAndHelpValue({
labelText: 'field_incidentio_token',
testId: 'incidentio-token-textbox',
});
expect(screen.getByTestId('incidentio-tip')).toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'incidentio_tip_link' }),
).toHaveAttribute(
'href',
'https://signoz.io/docs/alerts-management/notification-channel/incidentio/',
);
});
it('Should block save when the URL or token is missing', async () => {
const user = userEvent.setup();
await user.type(
screen.getByTestId('channel-name-textbox'),
'incidentio-channel',
);
await user.click(screen.getByTestId('save-channel-button'));
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'Error',
description: 'incidentio_required_fields',
}),
);
});
it('Should display an error when the URL is not an alert events URL', async () => {
const user = userEvent.setup();
await user.type(
screen.getByTestId('channel-name-textbox'),
'incidentio-channel',
);
await user.type(
screen.getByTestId('incidentio-url-textbox'),
'https://api.incident.io/v2/incidents',
);
await user.type(screen.getByTestId('incidentio-token-textbox'), 'tok-abc');
await user.click(screen.getByTestId('save-channel-button'));
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'Error',
description: 'incidentio_url_invalid',
}),
);
}, 15000);
it('Should send an incidentio_configs payload with prefilled defaults', async () => {
let requestBody: unknown;
server.use(
rest.post('http://localhost/api/v1/channels', async (req, res, ctx) => {
requestBody = await req.json();
return res(
ctx.status(201),
ctx.json({ status: 'success', data: 'channel created' }),
);
}),
);
const user = userEvent.setup();
await user.type(
screen.getByTestId('channel-name-textbox'),
'incidentio-channel',
);
await user.type(
screen.getByTestId('incidentio-url-textbox'),
incidentIOURL,
);
await user.type(screen.getByTestId('incidentio-token-textbox'), 'tok-abc');
await user.click(screen.getByTestId('incidentio-metadata-add'));
await user.type(screen.getByTestId('incidentio-metadata-key-0'), 'team');
await user.type(screen.getByTestId('incidentio-metadata-value-0'), 'core');
await user.click(screen.getByTestId('save-channel-button'));
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'Success',
description: 'channel_creation_done',
}),
);
expect(requestBody).toStrictEqual({
name: 'incidentio-channel',
incidentio_configs: [
{
url: incidentIOURL,
token: 'tok-abc',
send_resolved: true,
title: IncidentIOInitialConfig.title,
description: IncidentIOInitialConfig.description,
metadata: { team: 'core' },
},
],
});
}, 15000);
});
describe('Changing the channel type', () => {
async function selectType(
user: ReturnType<typeof userEvent.setup>,

View File

@@ -90,6 +90,40 @@ describe('EditAlertChannels save', () => {
await waitFor(() => expect(edit.calls).toHaveLength(1));
});
it('sends an incidentio_configs payload when editing an incident.io channel', async () => {
const edit = mockEditChannel();
render(
<EditAlertChannels
channelId="4"
initialValue={{
type: 'incidentio',
name: 'incidentio-channel',
url: 'https://api.incident.io/v2/alert_events/http/01M0D1JNVBGBGVTWX053EM12XV',
token: 'tok-abc',
send_resolved: true,
metadata: { env: 'prod' },
}}
/>,
);
const user = userEvent.setup();
await user.click(screen.getByTestId('save-channel-button'));
await waitFor(() => expect(edit.calls).toHaveLength(1));
expect(edit.calls[0].id).toBe('4');
expect(edit.calls[0].body).toStrictEqual({
name: 'incidentio-channel',
incidentio_configs: [
{
url: 'https://api.incident.io/v2/alert_events/http/01M0D1JNVBGBGVTWX053EM12XV',
token: 'tok-abc',
send_resolved: true,
metadata: { env: 'prod' },
},
],
});
});
it('persists send_resolved toggle in the edit request', async () => {
const edit = mockEditChannel();
render(

View File

@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
@@ -11,6 +12,8 @@ import DomainList from './Domains/DomainList';
import './Explorer.styles.scss';
function Explorer(): JSX.Element {
const quickFilterFieldApis = useSignalFieldApis();
useEffect(() => {
logEvent('API Monitoring: Landing page visited', {});
}, []);
@@ -26,6 +29,7 @@ function Explorer(): JSX.Element {
showFilterCollapse={false}
showQueryName={false}
handleFilterVisibilityChange={(): void => {}}
useFieldApis={quickFilterFieldApis}
/>
</section>
<DomainList />

View File

@@ -453,7 +453,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingActiveLicense &&
!isNull(activeLicense) &&
activeLicense?.event_queue?.event === LicenseEvent.DEFAULT
activeLicense?.eventQueue?.event === LicenseEvent.DEFAULT
) {
setShowPaymentFailedWarning(true);
}
@@ -820,7 +820,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
Your bill payment has failed. Your workspace will get suspended on{' '}
<span>
{getFormattedDateWithMinutes(
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
activeLicense?.eventQueue?.scheduledAt
? dayjs(activeLicense.eventQueue.scheduledAt).unix()
: dayjs().unix(),
)}
.
</span>

View File

@@ -15,6 +15,11 @@ import { getFormattedDate } from 'utils/timeUtils';
import BillingContainer from './BillingContainer';
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey', () => ({
__esModule: true,
default: jest.fn(() => ({ licenseKey: 'test-key', isLoading: false })),
}));
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({

View File

@@ -30,6 +30,7 @@ import useAxiosError from 'hooks/useAxiosError';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { isEmpty, pick } from 'lodash-es';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { useAppContext } from 'providers/App/App';
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
@@ -145,6 +146,7 @@ export default function BillingContainer(): JSX.Element {
activeLicense,
activeLicenseFetchError,
} = useAppContext();
const { licenseKey } = useActiveLicenseKey();
const { notifications } = useNotifications();
const handleError = useAxiosError();
@@ -207,9 +209,9 @@ export default function BillingContainer(): JSX.Element {
isFetching: isFetchingBillingData,
data: billingData,
} = useQuery([REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id], {
queryFn: () => getUsage(activeLicense?.key || ''),
queryFn: () => getUsage(licenseKey || ''),
onError: handleError,
enabled: activeLicense !== null,
enabled: !!licenseKey,
onSuccess: processUsageData,
});

View File

@@ -107,6 +107,7 @@ export enum ChannelType {
GoogleChat = 'googlechat',
Jira = 'jira',
JsmOps = 'jsmops',
IncidentIO = 'incidentio',
}
// LabelFilterStatement will be used for preparing filter conditions / matchers
@@ -159,6 +160,22 @@ export interface JiraChannel extends Channel {
reopen_duration?: string;
}
// IncidentIOChannel configures the incident.io alert channel, backed by an
// incident.io HTTP alert source (Alert Events V2 API).
export interface IncidentIOChannel extends Channel {
// per-source alert events URL, e.g.
// https://api.incident.io/v2/alert_events/http/<source_config_id>
url: string;
// the alert source's secret token
token: string;
// alert title template
title?: string;
// alert body template (markdown, rendered natively by incident.io)
description?: string;
// extra metadata pairs merged over the alert's labels (channel wins on clash)
metadata?: Record<string, string>;
}
// JsmOpsChannel configures the Jira Service Management Ops alert channel
// (ex-Opsgenie alert API). Auth is the JSM integration API key.
export interface JsmOpsChannel extends Channel {

View File

@@ -2,6 +2,7 @@ import {
ChannelType,
EmailChannel,
GoogleChatChannel,
IncidentIOChannel,
JiraChannel,
JsmOpsChannel,
MsTeamsChannel,
@@ -144,6 +145,29 @@ export const JsmOpsInitialConfig: Partial<JsmOpsChannel> = {
tags: ['signoz-alert'],
};
// mirrors DefaultIncidentIOTitleTemplate / DefaultIncidentIODescriptionTemplate
// in pkg/types/alertmanagertypes/incidentio.go, applied by the backend when
// title / description are left empty. send_resolved is seeded on so incident.io
// alerts resolve with the rule (the backend cannot default it).
export const IncidentIOInitialConfig: Partial<IncidentIOChannel> = {
send_resolved: true,
title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`,
description: `{{ range .Alerts -}}
**Alert:** {{ .Labels.alertname }}{{ if .Labels.severity }} ({{ .Labels.severity }}){{ end }}
{{ if .Annotations.summary }}**Summary:** {{ .Annotations.summary }}
{{ end }}{{ if .Annotations.description }}**Description:** {{ .Annotations.description }}
{{ end }}{{ if .GeneratorURL }}[View in SigNoz]({{ .GeneratorURL }})
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
{{ end }}{{ end }}`,
};
export const EmailInitialConfig: Partial<EmailChannel> = {
send_resolved: true,
html: `<!--
@@ -553,7 +577,8 @@ export const ChannelInitialConfig: Record<
EmailChannel &
GoogleChatChannel &
JiraChannel &
JsmOpsChannel
JsmOpsChannel &
IncidentIOChannel
>
> = {
[ChannelType.Slack]: SlackInitialConfig,
@@ -561,6 +586,7 @@ export const ChannelInitialConfig: Record<
[ChannelType.GoogleChat]: GoogleChatInitialConfig,
[ChannelType.Jira]: JiraInitialConfig,
[ChannelType.JsmOps]: JsmOpsInitialConfig,
[ChannelType.IncidentIO]: IncidentIOInitialConfig,
[ChannelType.Pagerduty]: PagerInitialConfig,
[ChannelType.Opsgenie]: OpsgenieInitialConfig,
[ChannelType.Email]: EmailInitialConfig,

View File

@@ -32,6 +32,7 @@ import {
ChannelType,
EmailChannel,
GoogleChatChannel,
IncidentIOChannel,
JiraChannel,
JsmOpsChannel,
MsTeamsChannel,
@@ -45,9 +46,11 @@ import { ChannelInitialConfig } from './defaults';
import {
isChannelType,
isValidGoogleChatWebhookURL,
isValidIncidentIOURL,
isValidJiraReopenDuration,
isValidJiraSiteURL,
prepareGoogleChatRequest,
prepareIncidentIORequest,
prepareJiraRequest,
prepareJsmOpsRequest,
} from './utils';
@@ -77,7 +80,8 @@ function CreateAlertChannels({
EmailChannel &
GoogleChatChannel &
JiraChannel &
JsmOpsChannel
JsmOpsChannel &
IncidentIOChannel
>
>(() => ({
send_resolved: true,
@@ -550,6 +554,56 @@ function CreateAlertChannels({
showErrorModal,
]);
const validateIncidentIOConfig = useCallback((): boolean => {
if (!selectedConfig.url || !selectedConfig.token) {
notifications.error({
message: 'Error',
description: t('incidentio_required_fields'),
});
return false;
}
if (!isValidIncidentIOURL(selectedConfig.url)) {
notifications.error({
message: 'Error',
description: t('incidentio_url_invalid'),
});
return false;
}
return true;
}, [selectedConfig.url, selectedConfig.token, notifications, t]);
const onIncidentIOHandler = useCallback(async () => {
if (!validateIncidentIOConfig()) {
return { status: 'failed', statusMessage: t('channel_creation_failed') };
}
setSavingState(true);
try {
await createChannel({ data: prepareIncidentIORequest(selectedConfig) });
notifications.success({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(toAPIError(error as ErrorType<RenderErrorResponseDTO>));
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [
validateIncidentIOConfig,
createChannel,
selectedConfig,
notifications,
t,
showErrorModal,
]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
if (!selectedConfig.name) {
@@ -570,6 +624,7 @@ function CreateAlertChannels({
[ChannelType.GoogleChat]: onGoogleChatHandler,
[ChannelType.Jira]: onJiraHandler,
[ChannelType.JsmOps]: onJsmOpsHandler,
[ChannelType.IncidentIO]: onIncidentIOHandler,
};
if (isChannelType(value)) {
@@ -604,6 +659,7 @@ function CreateAlertChannels({
onGoogleChatHandler,
onJiraHandler,
onJsmOpsHandler,
onIncidentIOHandler,
notifications,
t,
],
@@ -662,6 +718,13 @@ function CreateAlertChannels({
}
await testChannel({ data: prepareJsmOpsRequest(selectedConfig) });
break;
case ChannelType.IncidentIO:
if (!validateIncidentIOConfig()) {
setTestingState(false);
return;
}
await testChannel({ data: prepareIncidentIORequest(selectedConfig) });
break;
default:
notifications.error({
message: 'Error',
@@ -712,6 +775,7 @@ function CreateAlertChannels({
validateGoogleChatConfig,
validateJiraConfig,
validateJsmOpsConfig,
validateIncidentIOConfig,
testChannel,
notifications,
],

View File

@@ -1,4 +1,5 @@
import {
AlertmanagertypesIncidentIOReceiverConfigDTO,
AlertmanagertypesJiraReceiverConfigDTO,
AlertmanagertypesJSMOpsReceiverConfigDTO,
AlertmanagertypesPostableChannelDTO,
@@ -9,6 +10,7 @@ import {
import {
ChannelType,
GoogleChatChannel,
IncidentIOChannel,
JiraChannel,
JsmOpsChannel,
} from './config';
@@ -168,3 +170,50 @@ export const prepareJsmOpsRequest = (
jsmops_configs: [jsmops],
};
};
const INCIDENTIO_EVENTS_PATH_PREFIX = '/v2/alert_events/http/';
// the backend enforces the same rule, this is only for a nicer error experience
export const isValidIncidentIOURL = (url: string): boolean => {
try {
const { protocol, pathname } = new URL(url);
const idx = pathname.indexOf(INCIDENTIO_EVENTS_PATH_PREFIX);
return (
protocol === 'https:' &&
idx !== -1 &&
pathname.length > idx + INCIDENTIO_EVENTS_PATH_PREFIX.length
);
} catch {
return false;
}
};
// create, update and test all send the same body shape. Optional fields are
// omitted when empty so the backend applies its defaults.
export const prepareIncidentIORequest = (
config: Partial<IncidentIOChannel>,
): AlertmanagertypesPostableChannelDTO => {
const incidentio: AlertmanagertypesIncidentIOReceiverConfigDTO = {
url: config.url || '',
token: config.token || '',
send_resolved: config.send_resolved || false,
};
if (config.title) {
incidentio.title = config.title;
}
if (config.description) {
incidentio.description = config.description;
}
const metadata = Object.fromEntries(
Object.entries(config.metadata || {}).filter(([key]) => key.trim() !== ''),
);
if (Object.keys(metadata).length > 0) {
incidentio.metadata = metadata;
}
return {
name: config.name || '',
incidentio_configs: [incidentio],
};
};

View File

@@ -25,6 +25,7 @@ import {
ChannelType,
EmailChannel,
GoogleChatChannel,
IncidentIOChannel,
JiraChannel,
JsmOpsChannel,
MsTeamsChannel,
@@ -36,9 +37,11 @@ import {
} from 'container/CreateAlertChannels/config';
import {
isValidGoogleChatWebhookURL,
isValidIncidentIOURL,
isValidJiraReopenDuration,
isValidJiraSiteURL,
prepareGoogleChatRequest,
prepareIncidentIORequest,
prepareJiraRequest,
prepareJsmOpsRequest,
} from 'container/CreateAlertChannels/utils';
@@ -66,7 +69,8 @@ function EditAlertChannels({
EmailChannel &
GoogleChatChannel &
JiraChannel &
JsmOpsChannel
JsmOpsChannel &
IncidentIOChannel
>
>({
...initialValue,
@@ -578,6 +582,61 @@ function EditAlertChannels({
t,
]);
const validateIncidentIOConfig = useCallback((): string => {
if (!selectedConfig.url || !selectedConfig.token) {
return t('incidentio_required_fields');
}
if (!isValidIncidentIOURL(selectedConfig.url)) {
return t('incidentio_url_invalid');
}
return '';
}, [selectedConfig, t]);
const onIncidentIOEditHandler = useCallback(async () => {
const validationError = validateIncidentIOConfig();
if (validationError !== '') {
notifications.error({
message: 'Error',
description: validationError,
});
return { status: 'failed', statusMessage: validationError };
}
setSavingState(true);
try {
await updateChannel({
pathParams: { id },
data: prepareIncidentIORequest(selectedConfig),
});
notifications.success({
message: 'Success',
description: t('channel_edit_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_edit_done') };
} catch (error) {
const apiError = notifyError(error);
return {
status: 'failed',
statusMessage: apiError.getErrorMessage() || t('channel_edit_failed'),
};
} finally {
setSavingState(false);
}
}, [
validateIncidentIOConfig,
updateChannel,
id,
selectedConfig,
notifications,
notifyError,
t,
]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
let result;
@@ -599,6 +658,8 @@ function EditAlertChannels({
result = await onJiraEditHandler();
} else if (value === ChannelType.JsmOps) {
result = await onJsmOpsEditHandler();
} else if (value === ChannelType.IncidentIO) {
result = await onIncidentIOEditHandler();
}
logEvent('Alert Channel: Save channel', {
type: value,
@@ -620,6 +681,7 @@ function EditAlertChannels({
onGoogleChatEditHandler,
onJiraEditHandler,
onJsmOpsEditHandler,
onIncidentIOEditHandler,
],
);
@@ -701,6 +763,19 @@ function EditAlertChannels({
await testChannel({ data: prepareJsmOpsRequest(selectedConfig) });
break;
}
case ChannelType.IncidentIO: {
const validationError = validateIncidentIOConfig();
if (validationError !== '') {
notifications.error({
message: 'Error',
description: validationError,
});
setTestingState(false);
return;
}
await testChannel({ data: prepareIncidentIORequest(selectedConfig) });
break;
}
default:
notifications.error({
message: 'Error',
@@ -740,6 +815,7 @@ function EditAlertChannels({
validateGoogleChatConfig,
validateJiraConfig,
validateJsmOpsConfig,
validateIncidentIOConfig,
testChannel,
prepareWebhookRequest,
preparePagerRequest,

View File

@@ -452,15 +452,15 @@ function ExplorerOptions({
if (handleChangeSelectedView) {
handleChangeSelectedView(panelTypeToExplorerView[currentPanelType], {
query,
name,
id,
viewName: name,
viewKey: id,
});
} else {
// to remove this after traces cleanup
handleExplorerTabChange(currentPanelType, {
query,
name,
id,
viewName: name,
viewKey: id,
});
}
},

View File

@@ -0,0 +1,172 @@
import { Dispatch, SetStateAction, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Minus, Plus } from '@signozhq/icons';
import { Button, Form, Input } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { IncidentIOChannel } from '../../CreateAlertChannels/config';
interface MetadataRow {
key: string;
value: string;
}
function IncidentIOSettings({
setSelectedConfig,
initialMetadata,
}: IncidentIOProps): JSX.Element {
const { t } = useTranslation('channels');
const [metadataRows, setMetadataRows] = useState<MetadataRow[]>(() =>
Object.entries(initialMetadata || {}).map(([key, value]) => ({
key,
value,
})),
);
const update = (patch: Partial<IncidentIOChannel>): void =>
setSelectedConfig((value) => ({ ...value, ...patch }));
const syncMetadata = (rows: MetadataRow[]): void => {
setMetadataRows(rows);
update({
metadata: Object.fromEntries(
rows
.filter((row) => row.key.trim() !== '')
.map((row) => [row.key, row.value]),
),
});
};
return (
<>
<Typography.Text
color="muted"
size="sm"
testId="incidentio-tip"
style={{ display: 'block', marginBottom: 16 }}
>
{t('incidentio_tip')}{' '}
<Typography.Link
href="https://signoz.io/docs/alerts-management/notification-channel/incidentio/"
target="_blank"
rel="noopener noreferrer"
>
{t('incidentio_tip_link')}
</Typography.Link>
</Typography.Text>
<Form.Item
name="url"
label={t('field_incidentio_url')}
help={t('help_incidentio_url')}
required
>
<Input
onChange={(event): void => update({ url: event.target.value })}
data-testid="incidentio-url-textbox"
/>
</Form.Item>
<Form.Item
name="token"
label={t('field_incidentio_token')}
help={t('help_incidentio_token')}
required
>
<Input
type="password"
onChange={(event): void => update({ token: event.target.value })}
data-testid="incidentio-token-textbox"
/>
</Form.Item>
<Form.Item
name="title"
label={t('field_incidentio_title')}
help={t('help_incidentio_title')}
>
<Input.TextArea
rows={2}
onChange={(event): void => update({ title: event.target.value })}
data-testid="incidentio-title-textarea"
/>
</Form.Item>
<Form.Item
name="description"
label={t('field_incidentio_description')}
help={t('help_incidentio_description')}
>
<Input.TextArea
rows={6}
onChange={(event): void => update({ description: event.target.value })}
data-testid="incidentio-description-textarea"
/>
</Form.Item>
<Form.Item
label={t('field_incidentio_metadata')}
help={t('help_incidentio_metadata')}
>
{metadataRows.map((row, index) => (
// rows have no stable identity beyond their position
// eslint-disable-next-line react/no-array-index-key
<div key={index} style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<Input
placeholder={t('placeholder_incidentio_metadata_key')}
value={row.key}
onChange={(event): void =>
syncMetadata(
metadataRows.map((r, i) =>
i === index ? { ...r, key: event.target.value } : r,
),
)
}
data-testid={`incidentio-metadata-key-${index}`}
/>
<Input
placeholder={t('placeholder_incidentio_metadata_value')}
value={row.value}
onChange={(event): void =>
syncMetadata(
metadataRows.map((r, i) =>
i === index ? { ...r, value: event.target.value } : r,
),
)
}
data-testid={`incidentio-metadata-value-${index}`}
/>
<Button
icon={<Minus size={14} />}
onClick={(): void =>
syncMetadata(metadataRows.filter((_, i) => i !== index))
}
data-testid={`incidentio-metadata-remove-${index}`}
/>
</div>
))}
<Button
type="dashed"
icon={<Plus size={14} />}
onClick={(): void =>
syncMetadata([...metadataRows, { key: '', value: '' }])
}
data-testid="incidentio-metadata-add"
>
{t('button_incidentio_add_metadata')}
</Button>
</Form.Item>
</>
);
}
interface IncidentIOProps {
setSelectedConfig: Dispatch<SetStateAction<Partial<IncidentIOChannel>>>;
initialMetadata?: Record<string, string>;
}
IncidentIOSettings.defaultProps = {
initialMetadata: undefined,
};
export default IncidentIOSettings;

View File

@@ -10,6 +10,7 @@ import {
ChannelType,
EmailChannel,
GoogleChatChannel,
IncidentIOChannel,
JiraChannel,
JsmOpsChannel,
OpsgenieChannel,
@@ -21,6 +22,7 @@ import history from 'lib/history';
import EmailSettings from './Settings/Email';
import GoogleChatSettings from './Settings/GoogleChat';
import IncidentIOSettings from './Settings/IncidentIo';
import JiraSettings from './Settings/Jira';
import JsmOpsSettings from './Settings/JsmOps';
import MsTeamsSettings from './Settings/MsTeams';
@@ -61,6 +63,13 @@ function FormAlertChannels({
return <JiraSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.JsmOps:
return <JsmOpsSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.IncidentIO:
return (
<IncidentIOSettings
setSelectedConfig={setSelectedConfig}
initialMetadata={initialValue?.metadata as Record<string, string>}
/>
);
case ChannelType.Opsgenie:
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
case ChannelType.Email:
@@ -157,6 +166,14 @@ function FormAlertChannels({
<Select.Option value="jsmops" key="jsmops" data-testid="select-option">
Jira Service Management Ops
</Select.Option>
<Select.Option
value="incidentio"
key="incidentio"
data-testid="select-option"
>
incident.io
</Select.Option>
</Select>
</Form.Item>
@@ -207,7 +224,8 @@ interface FormAlertChannelsProps {
EmailChannel &
GoogleChatChannel &
JiraChannel &
JsmOpsChannel
JsmOpsChannel &
IncidentIOChannel
>
>
>;

View File

@@ -16,6 +16,8 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { StatusCodes } from 'http-status-codes';
import find from 'lodash-es/find';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { buildLicenseReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/license.permissions';
import { useAppContext } from 'providers/App/App';
import {
ErrorResponse,
@@ -673,17 +675,21 @@ function GeneralSettings({
</span>
</div>
{(showCustomDomainSettings || activeLicense?.key) && (
{(showCustomDomainSettings || activeLicense) && (
<div className="custom-domain-card">
{showCustomDomainSettings && <CustomDomainSettings />}
{showCustomDomainSettings && activeLicense?.key && (
{showCustomDomainSettings && activeLicense && (
<div className="custom-domain-card-divider" />
)}
{activeLicense?.key && (
<>
<LicenseKeyRow />
<LicenseRowDismissibleCallout />
</>
{activeLicense && (
<AuthZGuardContent
checks={[buildLicenseReadPermission(activeLicense.id)]}
>
<>
<LicenseKeyRow />
<LicenseRowDismissibleCallout />
</>
</AuthZGuardContent>
)}
</div>
)}

View File

@@ -2,16 +2,16 @@ import { useCopyToClipboard } from 'react-use';
import { Copy, KeyRound } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
import { useAppContext } from 'providers/App/App';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseKeyRow.styles.scss';
function LicenseKeyRow(): JSX.Element | null {
const { activeLicense } = useAppContext();
const { licenseKey } = useActiveLicenseKey();
const [, copyToClipboard] = useCopyToClipboard();
if (!activeLicense?.key) {
if (!licenseKey) {
return null;
}
@@ -27,16 +27,14 @@ function LicenseKeyRow(): JSX.Element | null {
<span className="license-key-row__label">SigNoz License Key</span>
</span>
<span className="license-key-row__value">
<code className="license-key-row__code">
{getMaskedKey(activeLicense.key)}
</code>
<code className="license-key-row__code">{getMaskedKey(licenseKey)}</code>
<Button
type="button"
size="sm"
aria-label="Copy license key"
data-testid="license-key-row-copy-btn"
className="license-key-row__copy-btn"
onClick={(): void => handleCopyLicenseKey(activeLicense.key)}
onClick={(): void => handleCopyLicenseKey(licenseKey)}
>
<Copy size={12} />
</Button>

View File

@@ -1,7 +1,13 @@
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import LicenseKeyRow from '../LicenseKeyRow';
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey');
const mockUseActiveLicenseKey = useActiveLicenseKey as jest.MockedFunction<
typeof useActiveLicenseKey
>;
const mockCopyToClipboard = jest.fn();
jest.mock('react-use', () => ({
@@ -23,20 +29,22 @@ describe('LicenseKeyRow', () => {
jest.clearAllMocks();
});
it('renders nothing when activeLicense key is absent', () => {
const { container } = render(<LicenseKeyRow />, undefined, {
appContextOverrides: { activeLicense: null },
it('renders nothing when the license key is absent', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: undefined,
isLoading: false,
});
const { container } = render(<LicenseKeyRow />);
expect(container).toBeEmptyDOMElement();
});
it('renders label and masked key when activeLicense key exists', () => {
render(<LicenseKeyRow />, undefined, {
appContextOverrides: {
activeLicense: { key: 'abcdefghij' } as any,
},
it('renders label and masked key when the license key exists', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'abcdefghij',
isLoading: false,
});
render(<LicenseKeyRow />);
expect(screen.getByText('SigNoz License Key')).toBeInTheDocument();
expect(screen.getByText('ab·······ij')).toBeInTheDocument();
@@ -45,6 +53,10 @@ describe('LicenseKeyRow', () => {
it('calls copyToClipboard and shows success toast when clipboard is available', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'test-key',
isLoading: false,
});
render(<LicenseKeyRow />);
await user.click(screen.getByRole('button', { name: /copy license key/i }));

View File

@@ -115,8 +115,8 @@ export default function SavedViews({
currentPanelType,
{
query,
name,
id,
viewName: name,
viewKey: id,
},
SOURCEPAGE_VS_ROUTES[selectedEntity],
);

View File

@@ -2,9 +2,9 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Button, Form } from 'antd';
import apply from 'api/v3/licenses/post';
import { activateLicense } from 'api/generated/services/licenses';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import {
@@ -26,7 +26,7 @@ function ApplyLicenseForm({
const isDisabled = isLoading || !key;
const onFinish = async (values: unknown | { key: string }): Promise<void> => {
const onFinish = async (values: unknown): Promise<void> => {
const params = values as { key: string };
if (params.key === '' || !params.key) {
notifications.error({
@@ -38,18 +38,19 @@ function ApplyLicenseForm({
setIsLoading(true);
try {
await apply({
await activateLicense({
key: params.key,
});
await Promise.all([licenseRefetch()]);
licenseRefetch();
notifications.success({
message: 'Success',
description: t('license_applied'),
});
} catch (e) {
const apiError = toAPIError(e as Parameters<typeof toAPIError>[0]);
notifications.error({
message: (e as APIError).getErrorCode(),
description: (e as APIError).getErrorMessage(),
message: apiError.getErrorCode(),
description: apiError.getErrorMessage(),
});
}
setIsLoading(false);

View File

@@ -16,7 +16,6 @@ import {
} from 'constants/antlrQueryConstants';
import { FeatureKeys } from 'constants/features';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
@@ -49,7 +48,6 @@ function BodyTitleRenderer({
const { featureFlags } = useAppContext();
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const { viewName } = useGetSavedViewParams();
const cleanedNodeKey = removeObjectFromString(nodeKey);
const isBodyJsonQueryEnabled =
@@ -123,8 +121,6 @@ function BodyTitleRenderer({
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
@@ -137,7 +133,6 @@ function BodyTitleRenderer({
stagedQuery,
updateQueriesData,
value,
viewName,
]);
const onClickHandler = (key: string): void => {

View File

@@ -12,7 +12,6 @@ import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import {
@@ -140,7 +139,6 @@ export default function TableViewActions(
const { pathname } = useLocation();
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const { viewName } = useGetSavedViewParams();
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
@@ -201,8 +199,6 @@ export default function TableViewActions(
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
@@ -214,7 +210,6 @@ export default function TableViewActions(
fieldType,
dataType,
handleChangeSelectedView,
viewName,
]);
const handleReplaceFilter = useCallback((): void => {
@@ -264,8 +259,6 @@ export default function TableViewActions(
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
@@ -278,7 +271,6 @@ export default function TableViewActions(
dataType,
fieldData,
handleChangeSelectedView,
viewName,
]);
// Memoize textToCopy computation

View File

@@ -272,8 +272,6 @@ describe('TableViewActions', () => {
expect(defaultProps.handleChangeSelectedView).toHaveBeenCalledWith(
ExplorerViews.TIMESERIES,
expect.objectContaining({
name: '',
id: 'test-query-id',
query: expect.objectContaining({
builder: expect.objectContaining({
queryData: expect.arrayContaining([

View File

@@ -5,7 +5,6 @@ import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
@@ -57,7 +56,6 @@ export function useLogAttributeActions({
const { pathname } = useLocation();
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const { featureFlags } = useAppContext();
const { viewName } = useGetSavedViewParams();
const isBodyJsonQueryEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
@@ -110,8 +108,6 @@ export function useLogAttributeActions({
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
handleChangeSelectedView?.(ExplorerViews.LIST, queryData);
@@ -120,7 +116,6 @@ export function useLogAttributeActions({
stagedQuery,
isBodyJsonQueryEnabled,
updateQueriesData,
viewName,
handleChangeSelectedView,
onApplyLogFilter,
],
@@ -147,8 +142,6 @@ export function useLogAttributeActions({
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
handleChangeSelectedView?.(ExplorerViews.TIMESERIES, queryData);
@@ -157,7 +150,6 @@ export function useLogAttributeActions({
stagedQuery,
isBodyJsonQueryEnabled,
updateQueriesData,
viewName,
handleChangeSelectedView,
],
);
@@ -183,8 +175,6 @@ export function useLogAttributeActions({
);
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
handleChangeSelectedView?.(ExplorerViews.LIST, queryData);
@@ -193,7 +183,6 @@ export function useLogAttributeActions({
stagedQuery,
isBodyJsonQueryEnabled,
updateQueriesData,
viewName,
handleChangeSelectedView,
],
);

View File

@@ -6,6 +6,7 @@ import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -31,6 +32,7 @@ import { splitQueryIntoOneChartPerQuery } from './utils';
import './Explorer.styles.scss';
function Explorer(): JSX.Element {
const quickFilterFieldApis = useSignalFieldApis();
const {
handleRunQuery,
stagedQuery,
@@ -141,6 +143,7 @@ function Explorer(): JSX.Element {
handleFilterVisibilityChange={(): void => {
setShowQuickFilters(!showQuickFilters);
}}
useFieldApis={quickFilterFieldApis}
/>
</div>

View File

@@ -78,8 +78,6 @@ function AllAttributes({
PANEL_TYPES.TIME_SERIES,
{
query: compositeQuery,
name: metricName,
id: metricName,
},
ROUTES.METRICS_EXPLORER_EXPLORER,
true,
@@ -109,8 +107,6 @@ function AllAttributes({
PANEL_TYPES.TIME_SERIES,
{
query: compositeQuery,
name: metricName,
id: metricName,
},
ROUTES.METRICS_EXPLORER_EXPLORER,
true,

View File

@@ -92,8 +92,6 @@ function MetricDetails({
PANEL_TYPES.TIME_SERIES,
{
query: compositeQuery,
name: metricName,
id: metricName,
},
ROUTES.METRICS_EXPLORER_EXPLORER,
true,

View File

@@ -3,13 +3,16 @@ import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { useNotifications } from 'hooks/useNotifications';
import { Copy } from '@signozhq/icons';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { buildLicenseReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/license.permissions';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseSection.styles.scss';
function LicenseSection(): JSX.Element | null {
const { activeLicense } = useAppContext();
function LicenseSectionContent(): JSX.Element | null {
const { licenseKey } = useActiveLicenseKey();
const { notifications } = useNotifications();
const [, handleCopyToClipboard] = useCopyToClipboard();
@@ -20,7 +23,41 @@ function LicenseSection(): JSX.Element | null {
});
};
if (!activeLicense?.key) {
if (!licenseKey) {
return null;
}
return (
<div className="license-section-content">
<div className="license-section-content-item">
<div className="license-section-content-item-title-action">
<span>License key</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Typography.Text code>{getMaskedKey(licenseKey)}</Typography.Text>
<Button
variant="link"
color="none"
aria-label="Copy license key"
data-testid="license-key-copy-btn"
onClick={(): void => handleCopyKey(licenseKey)}
>
<Copy size={14} />
</Button>
</span>
</div>
<div className="license-section-content-item-description">
Your SigNoz license key.
</div>
</div>
</div>
);
}
function LicenseSection(): JSX.Element | null {
const { activeLicense } = useAppContext();
if (!activeLicense) {
return <></>;
}
@@ -30,29 +67,9 @@ function LicenseSection(): JSX.Element | null {
<div className="license-section-title">License</div>
</div>
<div className="license-section-content">
<div className="license-section-content-item">
<div className="license-section-content-item-title-action">
<span>License key</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Typography.Text code>{getMaskedKey(activeLicense.key)}</Typography.Text>
<Button
variant="link"
color="none"
aria-label="Copy license key"
data-testid="license-key-copy-btn"
onClick={(): void => handleCopyKey(activeLicense.key)}
>
<Copy size={14} />
</Button>
</span>
</div>
<div className="license-section-content-item-description">
Your SigNoz license key.
</div>
</div>
</div>
<AuthZGuardContent checks={[buildLicenseReadPermission(activeLicense.id)]}>
<LicenseSectionContent />
</AuthZGuardContent>
</div>
);
}

View File

@@ -1,5 +1,11 @@
import userEvent from '@testing-library/user-event';
import MySettingsContainer from 'container/MySettings';
import useActiveLicenseKey from 'hooks/useActiveLicenseKey/useActiveLicenseKey';
import {
setupAuthzAdmin,
setupAuthzDenyAll,
} from 'lib/authz/utils/authz-test-utils';
import { server } from 'mocks-server/server';
import { logEventMock } from '__tests__/logEventMock';
import {
act,
@@ -12,6 +18,11 @@ import {
import APIError from 'types/api/error';
import { toast } from '@signozhq/ui/sonner';
jest.mock('hooks/useActiveLicenseKey/useActiveLicenseKey');
const mockUseActiveLicenseKey = useActiveLicenseKey as jest.MockedFunction<
typeof useActiveLicenseKey
>;
const toggleThemeFunction = jest.fn();
const copyToClipboardFn = jest.fn();
const editUserFn = jest.fn();
@@ -87,6 +98,10 @@ describe('MySettings Flows', () => {
jest.clearAllMocks();
editUserFn.mockResolvedValue({});
updateMyPasswordFn.mockResolvedValue({});
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'test-key',
isLoading: false,
});
render(<MySettingsContainer />);
});
@@ -361,17 +376,27 @@ describe('MySettings Flows', () => {
});
describe('License section', () => {
it('Should render license section content when license key exists', () => {
beforeEach(() => {
server.use(setupAuthzAdmin());
});
afterEach(() => {
server.resetHandlers();
});
it('Should render license section content when license key exists', async () => {
expect(screen.getByText('License')).toBeInTheDocument();
expect(screen.getByText('License key')).toBeInTheDocument();
await expect(screen.findByText('License key')).resolves.toBeInTheDocument();
expect(screen.getByText('Your SigNoz license key.')).toBeInTheDocument();
});
it('Should not render license section when license key is missing', () => {
it('Should not render license section when there is no active license', () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: undefined,
isLoading: false,
});
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: null,
},
appContextOverrides: { activeLicense: null },
});
const scoped = within(container);
@@ -382,41 +407,53 @@ describe('MySettings Flows', () => {
).not.toBeInTheDocument();
});
it('Should mask license key in the UI', () => {
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: {
key: 'abcd',
} as any,
},
});
it('Should show permission denied instead of the license key when read is denied', async () => {
server.use(setupAuthzDenyAll());
const { container } = render(<MySettingsContainer />);
expect(within(container).getByText('ab·······cd')).toBeInTheDocument();
const scoped = within(container);
await expect(
scoped.findByText(/not authorized/i),
).resolves.toBeInTheDocument();
expect(scoped.getByText('License')).toBeInTheDocument();
expect(scoped.queryByText('License key')).not.toBeInTheDocument();
});
it('Should not mask license key if it is too short', () => {
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: {
key: 'abc',
} as any,
},
it('Should mask license key in the UI', async () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'abcd',
isLoading: false,
});
const { container } = render(<MySettingsContainer />);
expect(within(container).getByText('abc')).toBeInTheDocument();
await expect(
within(container).findByText('ab·······cd'),
).resolves.toBeInTheDocument();
});
it('Should not mask license key if it is too short', async () => {
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'abc',
isLoading: false,
});
const { container } = render(<MySettingsContainer />);
await expect(
within(container).findByText('abc'),
).resolves.toBeInTheDocument();
});
it('Should copy license key and show success toast', async () => {
const user = userEvent.setup();
const { container } = render(<MySettingsContainer />, undefined, {
appContextOverrides: {
activeLicense: {
key: 'test-license-key-12345',
} as any,
},
mockUseActiveLicenseKey.mockReturnValue({
licenseKey: 'test-license-key-12345',
isLoading: false,
});
const { container } = render(<MySettingsContainer />);
await user.click(within(container).getByTestId('license-key-copy-btn'));
await user.click(
await within(container).findByTestId('license-key-copy-btn'),
);
await waitFor(() => {
expect(copyToClipboardFn).toHaveBeenCalledWith('test-license-key-12345');

View File

@@ -329,11 +329,12 @@ describe('transformTransactionGroupsToResourcePermissions', () => {
it('returns all resources from RESOURCE_ORDER even with empty transaction groups', () => {
const result = transformTransactionGroupsToResourcePermissions([]);
expect(result).toHaveLength(7);
expect(result).toHaveLength(8);
expect(result.map((r) => r.resourceKind)).toStrictEqual([
'factor-api-key',
'role',
'serviceaccount',
'license',
'logs',
'traces',
'metrics',
@@ -418,11 +419,12 @@ describe('createEmptyRolePermissions', () => {
it('creates permissions for all resources in RESOURCE_ORDER', () => {
const result = createEmptyRolePermissions();
expect(result).toHaveLength(7);
expect(result).toHaveLength(8);
expect(result.map((r) => r.resourceKind)).toStrictEqual([
'factor-api-key',
'role',
'serviceaccount',
'license',
'logs',
'traces',
'metrics',

View File

@@ -2,6 +2,7 @@ import {
Bot,
ChartLine,
DraftingCompass,
FileKey,
Gauge,
Key,
Logs,
@@ -61,6 +62,13 @@ export const RESOURCE_PANELS: Record<AuthZResource, ResourcePanelConfig> = {
'Type service account ID, separate multiple with comma or space',
docsAnchor: 'service-account',
},
license: {
label: 'Licenses',
description: 'Licenses of the workspace, including the license key.',
icon: FileKey,
selectorPlaceholder: 'Type license ID, separate multiple with comma or space',
docsAnchor: 'license',
},
logs: {
label: 'Logs',
description: 'Log data collected across the workspace.',

View File

@@ -0,0 +1,29 @@
import { useQuery, UseQueryResult } from 'react-query';
import {
getActiveLicense,
getGetActiveLicenseQueryKey,
} from 'api/generated/services/licenses';
import APIError from 'types/api/error';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
import { toAPIError } from 'utils/errorUtils';
import { toLicenseResModel } from './utils';
const useActiveLicense = (
isLoggedIn: boolean,
): UseQueryResult<LicenseResModel, APIError> =>
useQuery({
queryFn: async (): Promise<LicenseResModel> => {
try {
const response = await getActiveLicense();
return toLicenseResModel(response.data);
} catch (error) {
throw toAPIError(error as Parameters<typeof toAPIError>[0]);
}
},
queryKey: getGetActiveLicenseQueryKey(),
enabled: !!isLoggedIn,
retry: false,
});
export default useActiveLicense;

View File

@@ -0,0 +1,37 @@
import { LicensetypesGettableActiveLicenseDTO } from 'api/generated/services/sigNoz.schemas';
import {
LicenseEvent,
LicensePlatform,
LicenseResModel,
LicenseState,
LicenseStatus,
} from 'types/api/licensesV3/getActive';
export const toLicenseResModel = (
dto: LicensetypesGettableActiveLicenseDTO,
): LicenseResModel => ({
id: dto.id,
status: dto.status as LicenseStatus,
state: dto.state as LicenseState,
platform: dto.platform as LicensePlatform,
plan: {
id: dto.plan.id,
name: dto.plan.name,
description: dto.plan.description,
isActive: dto.plan.isActive,
createdAt: dto.plan.createdAt,
updatedAt: dto.plan.updatedAt,
},
eventQueue: {
event: dto.eventQueue.event as LicenseEvent,
status: dto.eventQueue.status,
scheduledAt: dto.eventQueue.scheduledAt,
createdAt: dto.eventQueue.createdAt,
updatedAt: dto.eventQueue.updatedAt,
},
freeUntil: dto.freeUntil,
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
validFrom: dto.validFrom,
validUntil: dto.validUntil,
});

View File

@@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { useGetLicense } from 'api/generated/services/licenses';
import { buildLicenseReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/license.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import { useAppContext } from 'providers/App/App';
interface UseActiveLicenseKey {
licenseKey: string | undefined;
isLoading: boolean;
}
const useActiveLicenseKey = (): UseActiveLicenseKey => {
const { activeLicense } = useAppContext();
const permissions = useMemo(
() => (activeLicense ? [buildLicenseReadPermission(activeLicense.id)] : []),
[activeLicense],
);
const { allowed, isLoading: isAuthZLoading } = useAuthZ(permissions, {
enabled: !!activeLicense,
});
const { data, isLoading: isLicenseLoading } = useGetLicense(
{ id: activeLicense?.id ?? '' },
{ query: { enabled: !!activeLicense && allowed } },
);
return {
licenseKey: data?.data.key,
isLoading:
!!activeLicense && (isAuthZLoading || (allowed && isLicenseLoading)),
};
};
export default useActiveLicenseKey;

View File

@@ -1,18 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import getActive from 'api/v3/licenses/active/get';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
const useActiveLicenseV3 = (isLoggedIn: boolean): UseLicense =>
useQuery({
queryFn: getActive,
queryKey: [REACT_QUERY_KEY.GET_ACTIVE_LICENSE_V3],
enabled: !!isLoggedIn,
retry: false,
});
type UseLicense = UseQueryResult<SuccessResponseV2<LicenseResModel>, APIError>;
export default useActiveLicenseV3;

View File

@@ -10,8 +10,8 @@ import { useGetSavedViewParams } from './saveViews/useGetSavedViewParams';
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
export interface ICurrentQueryData {
name: string;
id: string;
viewName?: string;
viewKey?: string;
query: Query;
}
@@ -57,6 +57,8 @@ export const useHandleExplorerTabChange = (): {
[currentQuery, updateAllQueriesOperators, updateQueriesData],
);
//TODO: this util is used not just to change explorer tab but also
// for changing just the query or saved view. consider renaming this.
const handleExplorerTabChange = useCallback(
(
type: string,
@@ -77,8 +79,8 @@ export const useHandleExplorerTabChange = (): {
query,
{
[QueryParams.panelTypes]: newPanelType,
[QueryParams.viewName]: currentQueryData?.name || viewName,
[QueryParams.viewKey]: currentQueryData?.id || viewKey,
[QueryParams.viewName]: currentQueryData?.viewName || viewName,
[QueryParams.viewKey]: currentQueryData?.viewKey || viewKey,
},
redirectToUrl,
undefined,
@@ -89,8 +91,8 @@ export const useHandleExplorerTabChange = (): {
query,
{
[QueryParams.panelTypes]: newPanelType,
[QueryParams.viewName]: currentQueryData?.name || viewName,
[QueryParams.viewKey]: currentQueryData?.id || viewKey,
[QueryParams.viewName]: currentQueryData?.viewName || viewName,
[QueryParams.viewKey]: currentQueryData?.viewKey || viewKey,
},
undefined,
undefined,

View File

@@ -8,6 +8,11 @@ export default {
type: 'metaresource',
allowedVerbs: ['create', 'delete', 'list', 'read', 'update'],
},
{
kind: 'license',
type: 'metaresource',
allowedVerbs: ['create', 'delete', 'list', 'read', 'update'],
},
{
kind: 'role',
type: 'role',

View File

@@ -0,0 +1,6 @@
import { buildPermission } from '../utils';
import type { BrandedPermission } from '../types';
// Resource-level — require a specific license id
export const buildLicenseReadPermission = (id: string): BrandedPermission =>
buildPermission('read', `license:${id}`);

View File

@@ -127,30 +127,30 @@ export function buildLicense(
overrides?: Partial<LicenseResModel>,
): LicenseResModel {
return {
key: 'test-key',
id: 'test-license-id',
status: LicenseStatus.VALID,
state: LicenseState.ACTIVATED,
platform: LicensePlatform.CLOUD,
event_queue: {
created_at: '0',
eventQueue: {
createdAt: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
scheduledAt: '0',
status: '',
updated_at: '0',
updatedAt: '0',
},
plan: {
created_at: '0',
id: '0',
createdAt: '0',
description: '',
is_active: true,
isActive: true,
name: '',
updated_at: '0',
updatedAt: '0',
},
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
created_at: '0',
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
createdAt: '0',
...overrides,
};
}

View File

@@ -4,114 +4,85 @@ export const quickFiltersListResponse = {
signal: 'logs',
filters: [
{
key: 'os.description',
dataType: 'string',
type: 'resource',
name: 'os.description',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'service.name',
dataType: 'string',
type: 'resource',
name: 'service.name',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'duration_nano',
dataType: 'float64',
type: 'tag',
name: 'duration_nano',
fieldDataType: 'float64',
fieldContext: 'attribute',
},
{
key: 'quantity',
dataType: 'float64',
type: 'tag',
name: 'quantity',
fieldDataType: 'float64',
fieldContext: 'attribute',
},
{
key: 'body',
dataType: 'string',
type: '',
name: 'body',
fieldDataType: 'string',
fieldContext: '',
},
{
key: 'deployment.environment',
dataType: 'string',
type: 'resource',
name: 'deployment.environment',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'service.namespace',
dataType: 'string',
type: 'resource',
name: 'service.namespace',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'k8s.namespace.name',
dataType: 'string',
type: 'resource',
name: 'k8s.namespace.name',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'service.instance.id',
dataType: 'string',
type: 'resource',
name: 'service.instance.id',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'k8s.pod.name',
dataType: 'string',
type: 'resource',
name: 'k8s.pod.name',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'process.owner',
dataType: 'string',
type: 'resource',
name: 'process.owner',
fieldDataType: 'string',
fieldContext: 'resource',
},
],
},
};
const otherFilterName = (name: string): { [k: string]: unknown[] } => ({
[name]: [
{ name, fieldContext: 'resource', fieldDataType: 'string', signal: 'logs' },
],
});
export const otherFiltersResponse = {
status: 'success',
data: {
attributes: [
{
key: 'service.name',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.deployment.name',
dataType: 'string',
type: 'resource',
},
{
key: 'deployment.environment',
dataType: 'string',
type: 'resource',
},
{
key: 'service.namespace',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.namespace.name',
dataType: 'string',
type: 'resource',
},
{
key: 'service.instance.id',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.pod.name',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.pod.uid',
dataType: 'string',
type: 'resource',
},
{
key: 'os.description',
dataType: 'string',
type: 'resource',
},
],
complete: true,
keys: {
...otherFilterName('service.name'),
...otherFilterName('k8s.deployment.name'),
...otherFilterName('deployment.environment'),
...otherFilterName('service.namespace'),
...otherFilterName('k8s.namespace.name'),
...otherFilterName('service.instance.id'),
...otherFilterName('k8s.pod.name'),
...otherFilterName('k8s.pod.uid'),
...otherFilterName('os.description'),
},
},
};

View File

@@ -8,6 +8,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import cx from 'classnames';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import RouteTab from 'components/RouteTab';
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
@@ -55,6 +56,8 @@ function AllErrors(): JSX.Element {
setShowFilters((prev) => !prev);
};
const quickFilterFieldApis = useSignalFieldApis();
return (
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
{showFilters && (
@@ -64,6 +67,7 @@ function AllErrors(): JSX.Element {
source={QuickFiltersSource.EXCEPTIONS}
signal={SignalType.EXCEPTIONS}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={quickFilterFieldApis}
/>
</section>
)}

View File

@@ -11,6 +11,7 @@ import ROUTES from 'constants/routes';
import {
ChannelType,
GoogleChatChannel,
IncidentIOChannel,
JiraChannel,
JsmOpsChannel,
MsTeamsChannel,
@@ -69,7 +70,8 @@ function ChannelsEdit(): JSX.Element {
MsTeamsChannel &
GoogleChatChannel &
JiraChannel &
JsmOpsChannel
JsmOpsChannel &
IncidentIOChannel
>;
} => {
let channel: Partial<
@@ -79,7 +81,8 @@ function ChannelsEdit(): JSX.Element {
MsTeamsChannel &
GoogleChatChannel &
JiraChannel &
JsmOpsChannel
JsmOpsChannel &
IncidentIOChannel
> = {
name: '',
};
@@ -135,6 +138,15 @@ function ChannelsEdit(): JSX.Element {
};
}
if (value && 'incidentio_configs' in value) {
const [incidentIOConfig] = value.incidentio_configs;
channel = incidentIOConfig;
return {
type: ChannelType.IncidentIO,
channel,
};
}
if (value && 'jsmops_configs' in value) {
const [jsmopsConfig] = value.jsmops_configs;
channel = jsmopsConfig;

View File

@@ -7,6 +7,7 @@ import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -74,6 +75,8 @@ function LogsExplorer(): JSX.Element {
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const quickFilterFieldApis = useSignalFieldApis();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const listQueryKeyRef = useRef<any>();
@@ -232,6 +235,7 @@ function LogsExplorer(): JSX.Element {
signal={SignalType.LOGS}
source={QuickFiltersSource.LOGS_EXPLORER}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={quickFilterFieldApis}
/>
</section>
)}

View File

@@ -209,8 +209,8 @@ function SaveView(): JSX.Element {
currentPanelType,
{
query,
name,
id,
viewName: name,
viewKey: id,
},
SOURCEPAGE_VS_ROUTES[sourcepage],
);

View File

@@ -504,7 +504,7 @@ jest.mock('hooks/useHandleExplorerTabChange', () => ({
let capturedPayload: QueryRangePayloadV5;
describe('TracesExplorer -', () => {
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/traces`;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/traces`;
const setupServer = (): void => {
server.use(

View File

@@ -8,6 +8,7 @@ import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import WarningPopover from 'components/WarningPopover/WarningPopover';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -128,6 +129,8 @@ function TracesExplorer(): JSX.Element {
);
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const quickFilterFieldApis = useSignalFieldApis();
const { safeNavigate } = useSafeNavigate();
const getExportToDashboardLink = useGetExportToDashboardLink();
@@ -267,6 +270,7 @@ function TracesExplorer(): JSX.Element {
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}
useFieldApis={quickFilterFieldApis}
/>
</Card>
<div

View File

@@ -22,7 +22,7 @@ import listUserPreferences from 'api/v1/user/preferences/list';
import getUserVersion from 'api/v1/version/get';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
import useActiveLicense from 'hooks/useActiveLicense/useActiveLicense';
import {
IsAdminPermission,
IsEditorPermission,
@@ -210,35 +210,34 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
}
}, [userData, isFetchingUserData]);
// fetcher for licenses v3
// fetcher for the active license
const {
data: activeLicenseData,
isFetching: isFetchingActiveLicense,
error: activeLicenseFetchError,
refetch: activeLicenseRefetch,
} = useActiveLicenseV3(isLoggedIn);
} = useActiveLicense(isLoggedIn);
useEffect(() => {
if (!isFetchingActiveLicense && activeLicenseData && activeLicenseData.data) {
setActiveLicense(activeLicenseData.data);
if (!isFetchingActiveLicense && activeLicenseData) {
setActiveLicense(activeLicenseData);
const isOnTrial = dayjs(
activeLicenseData.data.free_until || Date.now(),
).isAfter(dayjs());
const freeUntilUnix = dayjs(activeLicenseData.freeUntil).unix();
const scheduledAtUnix = dayjs(
activeLicenseData.eventQueue.scheduledAt,
).unix();
const trialInfo: TrialInfo = {
trialStart: activeLicenseData.data.valid_from,
trialEnd: dayjs(activeLicenseData.data.free_until || Date.now()).unix(),
onTrial: isOnTrial,
trialStart: activeLicenseData.validFrom,
trialEnd: freeUntilUnix > 0 ? freeUntilUnix : dayjs().unix(),
onTrial: dayjs(activeLicenseData.freeUntil).isAfter(dayjs()),
workSpaceBlock:
activeLicenseData.data.state === LicenseState.EVALUATION_EXPIRED &&
activeLicenseData.data.platform === LicensePlatform.CLOUD,
activeLicenseData.state === LicenseState.EVALUATION_EXPIRED &&
activeLicenseData.platform === LicensePlatform.CLOUD,
trialConvertedToSubscription:
activeLicenseData.data.state !== LicenseState.ISSUED &&
activeLicenseData.data.state !== LicenseState.EVALUATING &&
activeLicenseData.data.state !== LicenseState.EVALUATION_EXPIRED,
gracePeriodEnd: dayjs(
activeLicenseData.data.event_queue.scheduled_at || Date.now(),
).unix(),
activeLicenseData.state !== LicenseState.ISSUED &&
activeLicenseData.state !== LicenseState.EVALUATING &&
activeLicenseData.state !== LicenseState.EVALUATION_EXPIRED,
gracePeriodEnd: scheduledAtUnix > 0 ? scheduledAtUnix : dayjs().unix(),
};
setTrialInfo(trialInfo);

View File

@@ -158,30 +158,30 @@ export function getAppContextMock(
): IAppContext {
return {
activeLicense: {
key: 'test-key',
event_queue: {
created_at: '0',
id: 'test-license-id',
eventQueue: {
createdAt: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
scheduledAt: '0',
status: '',
updated_at: '0',
updatedAt: '0',
},
state: LicenseState.ACTIVATED,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,
created_at: '0',
createdAt: '0',
plan: {
created_at: '0',
id: '0',
createdAt: '0',
description: '',
is_active: true,
isActive: true,
name: '',
updated_at: '0',
updatedAt: '0',
},
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
},
trialInfo: {
trialStart: -1,

View File

@@ -1,10 +0,0 @@
import { License } from './def';
export interface Props {
key: string;
}
export interface PayloadProps {
status: string;
data: License;
}

View File

@@ -1,8 +0,0 @@
export interface License {
key: string;
ValidFrom: Date;
ValidUntil: Date;
planKey: string;
status: string;
isCurrent: boolean;
}

View File

@@ -1,57 +1,59 @@
export enum LicenseEvent {
NO_EVENT = '',
DEFAULT = 'DEFAULT',
DEFAULT = 'default',
}
export enum LicenseStatus {
SUSPENDED = 'SUSPENDED',
VALID = 'VALID',
INVALID = 'INVALID',
SUSPENDED = 'suspended',
VALID = 'valid',
INVALID = 'invalid',
}
export enum LicenseState {
DEFAULTED = 'DEFAULTED',
ACTIVATED = 'ACTIVATED',
EXPIRED = 'EXPIRED',
ISSUED = 'ISSUED',
EVALUATING = 'EVALUATING',
EVALUATION_EXPIRED = 'EVALUATION_EXPIRED',
TERMINATED = 'TERMINATED',
CANCELLED = 'CANCELLED',
DEFAULTED = 'defaulted',
ACTIVATED = 'activated',
EXPIRED = 'expired',
ISSUED = 'issued',
EVALUATING = 'evaluating',
EVALUATION_EXPIRED = 'evaluation_expired',
TERMINATED = 'terminated',
CANCELLED = 'cancelled',
}
export enum LicensePlatform {
SELF_HOSTED = 'SELF_HOSTED',
CLOUD = 'CLOUD',
SELF_HOSTED = 'self_hosted',
CLOUD = 'cloud',
}
export type LicensePlanResModel = {
id: string;
name: string;
description: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
};
export type LicenseEventQueueResModel = {
event: LicenseEvent;
status: string;
scheduled_at: string;
created_at: string;
updated_at: string;
scheduledAt: string;
createdAt: string;
updatedAt: string;
};
export type LicenseResModel = {
key: string;
id: string;
status: LicenseStatus;
state: LicenseState;
event_queue: LicenseEventQueueResModel;
platform: LicensePlatform;
created_at: string;
plan: {
created_at: string;
description: string;
is_active: boolean;
name: string;
updated_at: string;
};
plan_id: string;
free_until: string;
updated_at: string;
valid_from: number;
valid_until: number;
plan: LicensePlanResModel;
eventQueue: LicenseEventQueueResModel;
freeUntil: string;
createdAt: string;
updatedAt: string;
validFrom: number;
validUntil: number;
};
// Duplicate of old licenses API response, need to improve this later
@@ -63,8 +65,3 @@ export type TrialInfo = {
trialConvertedToSubscription: boolean;
gracePeriodEnd: number;
};
export interface PayloadProps {
data: LicenseEventQueueResModel;
status: string;
}

View File

@@ -1,14 +0,0 @@
export interface Filter {
key: string;
dataType: string;
type: string;
}
export interface Props {
signal: string;
}
export type PayloadProps = {
filters: Filter[];
signal: string;
};

View File

@@ -1,14 +0,0 @@
import { SignalType } from 'components/QuickFilters/types';
interface FilterType {
key: string;
datatype: string;
type: string;
}
export interface UpdateCustomFiltersProps {
data: {
filters: FilterType[];
signal: SignalType;
};
}

View File

@@ -184,4 +184,7 @@ export const routeWithInitialAuthZSupport = {
METRICS_EXPLORER_VOLUME_CONTROL: true,
METER_EXPLORER: true,
METER: true,
WORKSPACE_LOCKED: true,
WORKSPACE_SUSPENDED: true,
WORKSPACE_ACCESS_RESTRICTED: true,
} as const satisfies Partial<Record<keyof typeof ROUTES, true>>;

View File

@@ -0,0 +1,188 @@
package incidentio
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"maps"
"net/http"
"strings"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
const (
Integration = "incidentio"
// incident.io rejects payloads over 512 KB with a 413. Runes cap the
// description at 4 bytes each worst case (~400 KB), leaving headroom for
// the other fields.
maxDescriptionLenRunes = 100000
statusFiring = "firing"
statusResolved = "resolved"
)
// alertEvent is the body of incident.io's HTTP alert source endpoint
// (Alert Events V2 API). Title, status and deduplication_key are required;
// metadata values must be flat scalars. Repeat events for a firing key are
// dropped server-side and resolves for unknown keys are safe no-ops, so
// events are sent unconditionally.
type alertEvent struct {
Title string `json:"title"`
Description string `json:"description,omitempty"`
Status string `json:"status"`
DeduplicationKey string `json:"deduplication_key"`
SourceURL string `json:"source_url,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type Notifier struct {
conf *alertmanagertypes.IncidentIOReceiverConfig
tmpl *template.Template
logger *slog.Logger
client *http.Client
retrier *notify.Retrier
templater alertmanagertypes.Templater
}
func New(conf *alertmanagertypes.IncidentIOReceiverConfig, t *template.Template, l *slog.Logger, templater alertmanagertypes.Templater) (*Notifier, error) {
if conf.HTTPConfig == nil {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "incidentio http_config is nil")
}
client, err := notify.NewClientWithTracing(*conf.HTTPConfig, Integration)
if err != nil {
return nil, err
}
return &Notifier{
conf: conf,
tmpl: t,
logger: l,
client: client,
retrier: &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
templater: templater,
}, nil
}
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
firing := types.Alerts(as...).HasFiring()
n.logger.DebugContext(ctx, "sending incidentio notification", slog.String("group_key", key.String()), slog.Bool("firing", firing))
customTitle, customBody := alertmanagertemplate.ExtractTemplatesFromAnnotations(as)
result, err := n.templater.Expand(ctx, alertmanagertypes.ExpandRequest{
TitleTemplate: customTitle,
BodyTemplate: customBody,
DefaultTitleTemplate: n.conf.Title,
DefaultBodyTemplate: n.conf.Description,
}, as)
if err != nil {
return false, err
}
// title is required by the API; a channel title template can render empty,
// so fall back to the rule name, then to a static last resort.
title := result.Title
if strings.TrimSpace(title) == "" && len(as) > 0 {
title = string(as[0].Labels[ruletypes.LabelAlertName])
}
if strings.TrimSpace(title) == "" {
title = "SigNoz alert"
}
var parts []string
for _, body := range result.Body {
if body != "" {
parts = append(parts, body)
}
}
description := truncateRunes(strings.Join(parts, "\n\n---\n\n"), maxDescriptionLenRunes)
status := statusFiring
if !firing {
status = statusResolved
}
event := alertEvent{
Title: title,
Description: description,
Status: status,
DeduplicationKey: key.Hash(),
SourceURL: sourceURL(as),
Metadata: n.metadata(ctx, as),
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(event); err != nil {
return false, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.conf.URL, &buf)
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+string(n.conf.Token))
resp, err := n.client.Do(req) //nolint:bodyclose // notify.Drain closes the body
if err != nil {
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
return shouldRetry, nil
}
// metadata copies the group's common labels wholesale (the Opsgenie details
// precedent), so severity, ruleId and any user-defined rule labels arrive as
// flat strings ready for incident.io attribute mapping. Channel-configured
// pairs are template-expanded and laid on top (channel wins on key clash);
// a value that fails to expand is sent raw so delivery never breaks on it.
func (n *Notifier) metadata(ctx context.Context, as []*types.Alert) map[string]string {
data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
out := make(map[string]string, len(data.CommonLabels)+len(n.conf.Metadata))
maps.Copy(out, data.CommonLabels)
for k, v := range n.conf.Metadata {
expanded, err := n.tmpl.ExecuteTextString(v, data)
if err != nil {
n.logger.WarnContext(ctx, "failed to expand incidentio metadata value, sending it raw", slog.String("metadata_key", k))
expanded = v
}
out[k] = expanded
}
if len(out) == 0 {
return nil
}
return out
}
// sourceURL returns the per-rule SigNoz link from the ruleSource label, which
// is identical for every alert in the group.
func sourceURL(as []*types.Alert) string {
if len(as) == 0 {
return ""
}
return string(as[0].Labels[ruletypes.LabelRuleSource])
}
func truncateRunes(s string, max int) string {
runes := []rune(s)
if len(runes) <= max {
return s
}
return string(runes[:max-1]) + "…"
}

View File

@@ -0,0 +1,216 @@
package incidentio
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/notify/test"
"github.com/prometheus/alertmanager/types"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockIncidentIO struct {
srv *httptest.Server
mu sync.Mutex
events []alertEvent
auths []string
status int
}
func newMockIncidentIO(t *testing.T) *mockIncidentIO {
t.Helper()
m := &mockIncidentIO{status: http.StatusAccepted}
m.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ev alertEvent
_ = json.NewDecoder(r.Body).Decode(&ev)
m.mu.Lock()
m.events = append(m.events, ev)
m.auths = append(m.auths, r.Header.Get("Authorization"))
status := m.status
m.mu.Unlock()
w.WriteHeader(status)
if status == http.StatusAccepted {
_, _ = w.Write([]byte(`{"status":"accepted","message":"Event accepted for processing","deduplication_key":"` + ev.DeduplicationKey + `"}`))
} else {
_, _ = w.Write([]byte(`{"type":"validation_error","status":422,"errors":[{"code":"is_required","message":"Deduplication key is required"}]}`))
}
}))
t.Cleanup(m.srv.Close)
return m
}
func (m *mockIncidentIO) lastEvent(t *testing.T) alertEvent {
t.Helper()
m.mu.Lock()
defer m.mu.Unlock()
require.NotEmpty(t, m.events)
return m.events[len(m.events)-1]
}
func newNotifier(t *testing.T, m *mockIncidentIO) *Notifier {
t.Helper()
tmpl := test.CreateTmpl(t)
n, err := New(&alertmanagertypes.IncidentIOReceiverConfig{
URL: m.srv.URL + "/v2/alert_events/http/src-1",
Token: "tok-1",
Title: alertmanagertypes.DefaultIncidentIOTitleTemplate,
Description: alertmanagertypes.DefaultIncidentIODescriptionTemplate,
HTTPConfig: &commoncfg.HTTPClientConfig{},
}, tmpl, slog.New(slog.DiscardHandler), alertmanagertemplate.New(tmpl, slog.New(slog.DiscardHandler)))
require.NoError(t, err)
return n
}
func alert(firing bool) *types.Alert {
a := &types.Alert{Alert: model.Alert{
Labels: model.LabelSet{
"alertname": "HighCPU",
"severity": "critical",
"ruleSource": "https://signoz.example/alerts/edit?ruleId=1",
},
Annotations: model.LabelSet{"summary": "cpu high", "related_logs": "https://signoz.example/logs?q=1"},
GeneratorURL: "https://signoz.example/alerts/edit?ruleId=1",
StartsAt: time.Now().Add(-time.Minute),
}}
if firing {
a.EndsAt = time.Now().Add(time.Hour)
} else {
a.EndsAt = time.Now().Add(-time.Minute)
}
return a
}
func ctx() context.Context {
return notify.WithGroupKey(context.Background(), "test-incidentio")
}
func TestNotifyFiringEvent(t *testing.T) {
m := newMockIncidentIO(t)
retry, err := newNotifier(t, m).Notify(ctx(), alert(true))
require.NoError(t, err)
assert.False(t, retry)
ev := m.lastEvent(t)
assert.Equal(t, "[FIRING:1] HighCPU", ev.Title)
assert.Equal(t, "firing", ev.Status)
assert.NotEmpty(t, ev.DeduplicationKey)
assert.Equal(t, "https://signoz.example/alerts/edit?ruleId=1", ev.SourceURL)
assert.Contains(t, ev.Description, "**Alert:** HighCPU (critical)")
assert.Contains(t, ev.Description, "**Summary:** cpu high")
assert.Contains(t, ev.Description, "[View in SigNoz](https://signoz.example/alerts/edit?ruleId=1)")
assert.Contains(t, ev.Description, "[View related logs](https://signoz.example/logs?q=1)")
assert.Equal(t, map[string]string{
"alertname": "HighCPU",
"severity": "critical",
"ruleSource": "https://signoz.example/alerts/edit?ruleId=1",
}, ev.Metadata)
assert.Equal(t, "Bearer tok-1", m.auths[0])
}
func TestNotifyResolvedEventReusesDedupKey(t *testing.T) {
m := newMockIncidentIO(t)
n := newNotifier(t, m)
_, err := n.Notify(ctx(), alert(true))
require.NoError(t, err)
firingKey := m.lastEvent(t).DeduplicationKey
_, err = n.Notify(ctx(), alert(false))
require.NoError(t, err)
ev := m.lastEvent(t)
assert.Equal(t, "resolved", ev.Status)
assert.Equal(t, firingKey, ev.DeduplicationKey)
}
func TestNotifyPermanentFailureDoesNotRetry(t *testing.T) {
m := newMockIncidentIO(t)
m.status = http.StatusUnprocessableEntity
retry, err := newNotifier(t, m).Notify(ctx(), alert(true))
require.Error(t, err)
assert.False(t, retry)
assert.Contains(t, err.Error(), "Deduplication key is required") // response body surfaces to the user
}
func TestNotifyRateLimitRetries(t *testing.T) {
m := newMockIncidentIO(t)
m.status = http.StatusTooManyRequests
retry, err := newNotifier(t, m).Notify(ctx(), alert(true))
require.Error(t, err)
assert.True(t, retry)
}
func TestNotifyMergesChannelMetadata(t *testing.T) {
m := newMockIncidentIO(t)
tmpl := test.CreateTmpl(t)
n, err := New(&alertmanagertypes.IncidentIOReceiverConfig{
URL: m.srv.URL + "/v2/alert_events/http/src-1",
Token: "tok-1",
Title: alertmanagertypes.DefaultIncidentIOTitleTemplate,
Description: alertmanagertypes.DefaultIncidentIODescriptionTemplate,
HTTPConfig: &commoncfg.HTTPClientConfig{},
Metadata: map[string]string{
"env": "prod",
"sev": "{{ .CommonLabels.severity }}",
"alertname": "channel-wins",
"broken": "{{ .Nope",
},
}, tmpl, slog.New(slog.DiscardHandler), alertmanagertemplate.New(tmpl, slog.New(slog.DiscardHandler)))
require.NoError(t, err)
_, err = n.Notify(ctx(), alert(true))
require.NoError(t, err) // a broken metadata template must not fail delivery
md := m.lastEvent(t).Metadata
assert.Equal(t, "prod", md["env"])
assert.Equal(t, "critical", md["sev"]) // values are template-expanded
assert.Equal(t, "channel-wins", md["alertname"]) // channel overrides the rule label
assert.Equal(t, "{{ .Nope", md["broken"]) // unexpandable value sent raw
assert.Equal(t, "critical", md["severity"]) // rule labels still present
}
func TestNotifyEmptyTitleFallsBackToRuleName(t *testing.T) {
m := newMockIncidentIO(t)
tmpl := test.CreateTmpl(t)
n, err := New(&alertmanagertypes.IncidentIOReceiverConfig{
URL: m.srv.URL + "/v2/alert_events/http/src-1",
Token: "tok-1",
Title: `{{ .CommonLabels.nonexistent }}`,
Description: alertmanagertypes.DefaultIncidentIODescriptionTemplate,
HTTPConfig: &commoncfg.HTTPClientConfig{},
}, tmpl, slog.New(slog.DiscardHandler), alertmanagertemplate.New(tmpl, slog.New(slog.DiscardHandler)))
require.NoError(t, err)
_, err = n.Notify(ctx(), alert(true))
require.NoError(t, err)
assert.Equal(t, "HighCPU", m.lastEvent(t).Title)
}
func TestNotifyTruncatesLongDescription(t *testing.T) {
m := newMockIncidentIO(t)
a := alert(true)
a.Annotations["description"] = model.LabelValue(strings.Repeat("x", maxDescriptionLenRunes+1000))
_, err := newNotifier(t, m).Notify(ctx(), a)
require.NoError(t, err)
desc := []rune(m.lastEvent(t).Description)
assert.LessOrEqual(t, len(desc), maxDescriptionLenRunes)
assert.Equal(t, '…', desc[len(desc)-1])
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/email"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/googlechat"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/incidentio"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/jira"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/jsmops"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/msteamsv2"
@@ -30,6 +31,7 @@ var customNotifierIntegrations = []string{
googlechat.Integration,
jira.Integration,
jsmops.Integration,
incidentio.Integration,
}
func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Template, logger *slog.Logger, templater alertmanagertypes.Templater) ([]notify.Integration, error) {
@@ -95,6 +97,11 @@ func NewReceiverIntegrations(nc *alertmanagertypes.Receiver, tmpl *template.Temp
return jsmops.New(c, tmpl, l, templater, true)
})
}
for i, c := range nc.IncidentIOConfigs {
add(incidentio.Integration, i, c, func(l *slog.Logger) (notify.Notifier, error) {
return incidentio.New(c, tmpl, l, templater)
})
}
if errs.Len() > 0 {
return nil, &errs

View File

@@ -75,7 +75,7 @@ func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertyp
NewInsert().
Model(channel).
Exec(ctx); err != nil {
return err
return store.sqlstore.WrapAlreadyExistsErrf(err, alertmanagertypes.ErrCodeAlertmanagerChannelAlreadyExists, "channel with name %q already exists", channel.Name)
}
return nil

View File

@@ -0,0 +1,71 @@
package sqlalertmanagerstore
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func TestCreateChannelRejectsDuplicateNameInSameOrg(t *testing.T) {
sqlstore := newTestStore(t)
_, err := sqlstore.BunDB().NewCreateTable().
Model((*alertmanagertypes.Channel)(nil)).
IfNotExists().
Exec(t.Context())
require.NoError(t, err)
_, err = sqlstore.BunDB().NewCreateIndex().
Model((*alertmanagertypes.Channel)(nil)).
Index("notification_channel_org_id_name_idx").
Column("org_id", "name").
Unique().
Exec(t.Context())
require.NoError(t, err)
store := NewConfigStore(sqlstore)
orgID := valuer.GenerateUUID().StringValue()
now := time.Now().UTC()
firstChannel := &alertmanagertypes.Channel{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
Name: "shared-name",
DisplayName: "First Channel",
Type: "slack",
Data: `{"name":"First Channel","slack_configs":[{"api_url":"https://hooks.slack.com/services/first"}]}`,
OrgID: orgID,
}
require.NoError(t, store.CreateChannel(t.Context(), firstChannel))
duplicateChannel := &alertmanagertypes.Channel{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
Name: "shared-name",
DisplayName: "Second Channel",
Type: "slack",
Data: `{"name":"Second Channel","slack_configs":[{"api_url":"https://hooks.slack.com/services/second"}]}`,
OrgID: orgID,
}
err = store.CreateChannel(t.Context(), duplicateChannel)
require.Error(t, err)
assert.True(t, errors.Ast(err, errors.TypeAlreadyExists))
otherOrgChannel := &alertmanagertypes.Channel{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
Name: "shared-name",
DisplayName: "Second Channel",
Type: "slack",
Data: `{"name":"Second Channel","slack_configs":[{"api_url":"https://hooks.slack.com/services/second"}]}`,
OrgID: valuer.GenerateUUID().StringValue(),
}
assert.NoError(t, store.CreateChannel(t.Context(), otherOrgChannel))
}

View File

@@ -187,7 +187,7 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
}
// Check if channel is referenced by any route policy (rule-based or policy-based)
policies, err := provider.notificationManager.GetRoutePoliciesByChannel(ctx, orgID, channel.Name)
policies, err := provider.notificationManager.GetRoutePoliciesByChannel(ctx, orgID, channel.DisplayName)
if err != nil {
return err
}
@@ -198,7 +198,7 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
}
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"channel %q cannot be deleted because it is used by the following routing policies: %v",
channel.Name, names)
channel.DisplayName, names)
}
config, err := provider.configStore.Get(ctx, orgID)
@@ -206,7 +206,7 @@ func (provider *provider) DeleteChannelByID(ctx context.Context, orgID string, c
return err
}
if err := config.DeleteReceiver(channel.Name); err != nil {
if err := config.DeleteReceiver(channel.DisplayName); err != nil {
return err
}

View File

@@ -97,23 +97,57 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.GetIngestionKey), handler.OpenAPIDef{
ID: "GetIngestionKey",
Tags: []string{"gateway"},
Summary: "Get ingestion key for workspace",
Description: "This endpoint returns an ingestion key for the workspace",
Request: nil,
RequestContentType: "",
Response: new(gatewaytypes.IngestionKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeprecatedCreateIngestionKeyLimit), handler.OpenAPIDef{
ID: "CreateIngestionKeyLimit",
Tags: []string{"gateway"},
Summary: "Create limit for the ingestion key",
Description: "This endpoint creates an ingestion key limit",
Request: new(gatewaytypes.PostableIngestionKeyLimit),
Request: new(gatewaytypes.DeprecatedPostableIngestionKeyLimit),
RequestContentType: "application/json",
Response: new(gatewaytypes.GettableCreatedIngestionKeyLimit),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{},
Deprecated: false,
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.GetIngestionKeyLimits), handler.OpenAPIDef{
ID: "GetIngestionKeyLimits",
Tags: []string{"gateway"},
Summary: "Get limits for the ingestion key",
Description: "This endpoint returns the ingestion limits for an ingestion key",
Request: nil,
RequestContentType: "",
Response: new([]gatewaytypes.Limit),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
ID: "UpdateIngestionKeyLimit",
Tags: []string{"gateway"},
@@ -125,7 +159,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
@@ -142,6 +176,74 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_limits", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
ID: "CreateIngestionLimit",
Tags: []string{"gateway"},
Summary: "Create ingestion limit",
Description: "This endpoint creates an ingestion limit for the ingestion key referenced by keyId",
Request: new(gatewaytypes.PostableIngestionKeyLimit),
RequestContentType: "application/json",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.GetIngestionKeyLimit), handler.OpenAPIDef{
ID: "GetIngestionLimit",
Tags: []string{"gateway"},
Summary: "Get ingestion limit",
Description: "This endpoint returns an ingestion limit",
Request: nil,
RequestContentType: "",
Response: new(gatewaytypes.Limit),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
ID: "UpdateIngestionLimit",
Tags: []string{"gateway"},
Summary: "Update ingestion limit",
Description: "This endpoint updates an ingestion limit",
Request: new(gatewaytypes.UpdatableIngestionKeyLimit),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPatch).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/gateway/ingestion_limits/{limitId}", handler.New(provider.authzMiddleware.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
ID: "DeleteIngestionLimit",
Tags: []string{"gateway"},
Summary: "Delete ingestion limit",
Description: "This endpoint deletes an ingestion limit",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {

View File

@@ -0,0 +1,219 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/gorilla/mux"
)
func (provider *provider) addLicensingRoutes(router *mux.Router) error {
if err := router.Handle("/api/v4/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Create, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ActivateLicense",
Tags: []string{"licenses"},
Summary: "Activate a license.",
Description: "This endpoint validates the license key with the upstream server and activates the license for the organization.",
Request: new(licensetypes.PostableLicense),
RequestContentType: "application/json",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v3/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.ActivateDeprecated, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ActivateLicenseDeprecated",
Tags: []string{"licenses"},
Summary: "Activate a license.",
Description: "This endpoint validates the license key with the upstream server and activates the license for the organization.",
Request: new(licensetypes.PostableLicense),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusAccepted,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusConflict},
Deprecated: true,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v3/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.RefreshDeprecated, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "RefreshLicenseDeprecated",
Tags: []string{"licenses"},
Summary: "Refresh a license.",
Description: "This endpoint refreshes the active license of the organization from the upstream server.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: true,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.List, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "ListLicenses",
Tags: []string{"licenses"},
Summary: "List licenses.",
Description: "This endpoint lists all the licenses of the organization.",
Request: nil,
RequestContentType: "",
Response: make([]*licensetypes.GettableLicense, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/active", handler.New(provider.authzMiddleware.OpenAccess(provider.licensingHandler.GetActive), handler.OpenAPIDef{
ID: "GetActiveLicense",
Tags: []string{"licenses"},
Summary: "Get the active license.",
Description: "This endpoint gets the active license of the organization.",
Request: nil,
RequestContentType: "",
Response: new(licensetypes.GettableActiveLicense),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusNotImplemented},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes(nil),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Get, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "GetLicense",
Tags: []string{"licenses"},
Summary: "Get a license.",
Description: "This endpoint gets the license by id.",
Request: nil,
RequestContentType: "",
Response: new(licensetypes.GettableLicenseWithKey),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Refresh, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "RefreshLicense",
Tags: []string{"licenses"},
Summary: "Refresh a license.",
Description: "This endpoint refreshes the active license of the organization from the upstream server.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v4/licenses/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.licensingHandler.Delete, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "DeleteLicense",
Tags: []string{"licenses"},
Summary: "Delete a license.",
Description: "This endpoint deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceLicense.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceLicense,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aiobservability"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -24,6 +25,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/preference"
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
@@ -68,6 +70,7 @@ type provider struct {
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
licensingHandler licensing.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
serviceAccountGetter serviceaccount.Getter
@@ -82,6 +85,8 @@ type provider struct {
llmPricingRuleHandler llmpricingrule.Handler
statsHandler statsreporter.Handler
savedViewHandler savedview.Handler
quickFilterModule quickfilter.Module
quickFilterHandler quickfilter.Handler
}
func NewFactory(
@@ -107,6 +112,7 @@ func NewFactory(
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
licensingHandler licensing.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
serviceAccountGetter serviceaccount.Getter,
@@ -121,6 +127,8 @@ func NewFactory(
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
quickFilterModule quickfilter.Module,
quickFilterHandler quickfilter.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -149,6 +157,7 @@ func NewFactory(
authzHandler,
rawDataExportHandler,
zeusHandler,
licensingHandler,
querierHandler,
serviceAccountHandler,
serviceAccountGetter,
@@ -163,6 +172,8 @@ func NewFactory(
rulerHandler,
statsHandler,
savedViewHandler,
quickFilterModule,
quickFilterHandler,
)
})
}
@@ -193,6 +204,7 @@ func newProvider(
authzHandler authz.Handler,
rawDataExportHandler rawdataexport.Handler,
zeusHandler zeus.Handler,
licensingHandler licensing.Handler,
querierHandler querier.Handler,
serviceAccountHandler serviceaccount.Handler,
serviceAccountGetter serviceaccount.Getter,
@@ -207,6 +219,8 @@ func newProvider(
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
quickFilterModule quickfilter.Module,
quickFilterHandler quickfilter.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -236,6 +250,7 @@ func newProvider(
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
licensingHandler: licensingHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
serviceAccountGetter: serviceAccountGetter,
@@ -250,6 +265,8 @@ func newProvider(
llmPricingRuleHandler: llmPricingRuleHandler,
statsHandler: statsHandler,
savedViewHandler: savedViewHandler,
quickFilterModule: quickFilterModule,
quickFilterHandler: quickFilterHandler,
}
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
@@ -338,6 +355,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addLicensingRoutes(router); err != nil {
return err
}
if err := provider.addZeusRoutes(router); err != nil {
return err
}
@@ -394,6 +415,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addQuickFilterRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,120 @@
package signozapiserver
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/quickfiltertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (provider *provider) addQuickFilterRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/quick_filters", handler.New(
provider.authzMiddleware.CheckResources(provider.quickFilterHandler.ListQuickFiltersV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListQuickFilters",
Tags: []string{"quick_filter"},
Summary: "List quick filters",
Description: "Returns the org's quick filters for every source, each filter as a telemetry field key.",
Request: nil,
RequestContentType: "",
Response: make([]*quickfiltertypes.SourceFilters, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceQuickFilter.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceQuickFilter,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/quick_filters/{source}", handler.New(
provider.authzMiddleware.CheckResources(provider.quickFilterHandler.GetQuickFiltersV2, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetQuickFilters",
Tags: []string{"quick_filter"},
Summary: "Get a source's quick filters",
Description: "Returns the org's quick filters for one source, each filter as a telemetry field key.",
Request: nil,
RequestContentType: "",
Response: new(quickfiltertypes.SourceFilters),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceQuickFilter.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceQuickFilter,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("source"),
Selector: provider.quickFilterSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/quick_filters/{source}", handler.New(
provider.authzMiddleware.CheckResources(provider.quickFilterHandler.UpdateQuickFiltersV2, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "UpdateQuickFilters",
Tags: []string{"quick_filter"},
Summary: "Update quick filters",
Description: "Replaces the org's quick filters for the source named in the path.",
Request: new(quickfiltertypes.UpdatableQuickFilters),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceQuickFilter.Scope(coretypes.VerbUpdate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceQuickFilter,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("source"),
Selector: provider.quickFilterSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
return nil
}
func (provider *provider) quickFilterSelector(ctx context.Context, resource coretypes.Resource, source string, orgID valuer.UUID) ([]coretypes.Selector, error) {
validatedSource, err := quickfiltertypes.NewSource(source)
if err != nil {
return nil, err
}
// A source can have no stored row yet: GET serves it as empty and PUT
// creates it, so only the wildcard grant applies until the row exists.
quickFilter, err := provider.quickFilterModule.Get(ctx, orgID, validatedSource)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return []coretypes.Selector{resource.Type().MustSelector(coretypes.WildCardSelectorString)}, nil
}
return nil, err
}
return []coretypes.Selector{
resource.Type().MustSelector(quickFilter.ID.StringValue()),
resource.Type().MustSelector(coretypes.WildCardSelectorString),
}, nil
}

View File

@@ -22,6 +22,9 @@ type Gateway interface {
// Search Ingestion Keys by Name (this is supposed to be for the current user but for now in gateway code this is ignoring the consumer user)
SearchIngestionKeysByName(ctx context.Context, orgID valuer.UUID, name string, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error)
// Get Ingestion Key
GetIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string) (*gatewaytypes.IngestionKey, error)
// Create Ingestion Key
CreateIngestionKey(ctx context.Context, orgID valuer.UUID, name string, tags []string, expiresAt time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error)
@@ -34,6 +37,12 @@ type Gateway interface {
// Create Ingestion Key Limit
CreateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, keyID string, signal string, limitConfig gatewaytypes.LimitConfig, tags []string) (*gatewaytypes.GettableCreatedIngestionKeyLimit, error)
// Get Ingestion Key Limit
GetIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string) (*gatewaytypes.Limit, error)
// Get Ingestion Key Limits
GetIngestionKeyLimits(ctx context.Context, orgID valuer.UUID, keyID string) ([]gatewaytypes.Limit, error)
// Update Ingestion Key Limit
UpdateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string, limitConfig gatewaytypes.LimitConfig, tags []string) error
@@ -46,6 +55,8 @@ type Handler interface {
SearchIngestionKeys(http.ResponseWriter, *http.Request)
GetIngestionKey(http.ResponseWriter, *http.Request)
CreateIngestionKey(http.ResponseWriter, *http.Request)
UpdateIngestionKey(http.ResponseWriter, *http.Request)
@@ -54,7 +65,13 @@ type Handler interface {
CreateIngestionKeyLimit(http.ResponseWriter, *http.Request)
GetIngestionKeyLimit(http.ResponseWriter, *http.Request)
GetIngestionKeyLimits(http.ResponseWriter, *http.Request)
UpdateIngestionKeyLimit(http.ResponseWriter, *http.Request)
DeleteIngestionKeyLimit(http.ResponseWriter, *http.Request)
DeprecatedCreateIngestionKeyLimit(http.ResponseWriter, *http.Request)
}

View File

@@ -1,11 +1,11 @@
package gateway
import (
"encoding/json"
"net/http"
"strconv"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
@@ -111,8 +111,8 @@ func (handler *handler) CreateIngestionKey(rw http.ResponseWriter, r *http.Reque
orgID := valuer.MustNewUUID(claims.OrgID)
var req gatewaytypes.PostableIngestionKey
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
@@ -143,8 +143,8 @@ func (handler *handler) UpdateIngestionKey(rw http.ResponseWriter, r *http.Reque
}
var req gatewaytypes.PostableIngestionKey
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
@@ -183,7 +183,7 @@ func (handler *handler) DeleteIngestionKey(rw http.ResponseWriter, r *http.Reque
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) CreateIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) DeprecatedCreateIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
@@ -200,9 +200,9 @@ func (handler *handler) CreateIngestionKeyLimit(rw http.ResponseWriter, r *http.
return
}
var req gatewaytypes.PostableIngestionKeyLimit
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
var req gatewaytypes.DeprecatedPostableIngestionKeyLimit
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
@@ -233,8 +233,8 @@ func (handler *handler) UpdateIngestionKeyLimit(rw http.ResponseWriter, r *http.
}
var req gatewaytypes.UpdatableIngestionKeyLimit
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
@@ -273,6 +273,115 @@ func (handler *handler) DeleteIngestionKeyLimit(rw http.ResponseWriter, r *http.
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) CreateIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
var req gatewaytypes.PostableIngestionKeyLimit
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
render.Error(rw, err)
return
}
if req.KeyID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "keyId is required"))
return
}
response, err := handler.gateway.CreateIngestionKeyLimit(ctx, orgID, req.KeyID, req.Signal, req.Config, req.Tags)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, response)
}
func (handler *handler) GetIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
limitID := mux.Vars(r)["limitId"]
if limitID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "limitId is required"))
return
}
response, err := handler.gateway.GetIngestionKeyLimit(ctx, orgID, limitID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) GetIngestionKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
keyID := mux.Vars(r)["keyId"]
if keyID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "keyId is required"))
return
}
response, err := handler.gateway.GetIngestionKey(ctx, orgID, keyID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) GetIngestionKeyLimits(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
keyID := mux.Vars(r)["keyId"]
if keyID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "keyId is required"))
return
}
response, err := handler.gateway.GetIngestionKeyLimits(ctx, orgID, keyID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, response)
}
func parseIntWithDefaultValue(value string, defaultValue int) (int, error) {
if value == "" {
return defaultValue, nil

View File

@@ -31,6 +31,10 @@ func (p *provider) SearchIngestionKeysByName(_ context.Context, _ valuer.UUID, _
return nil, errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}
func (p *provider) GetIngestionKey(_ context.Context, _ valuer.UUID, _ string) (*gatewaytypes.IngestionKey, error) {
return nil, errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}
func (p *provider) CreateIngestionKey(_ context.Context, _ valuer.UUID, _ string, _ []string, _ time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error) {
return nil, errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}
@@ -47,6 +51,14 @@ func (p *provider) CreateIngestionKeyLimit(_ context.Context, _ valuer.UUID, _ s
return nil, errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}
func (p *provider) GetIngestionKeyLimit(_ context.Context, _ valuer.UUID, _ string) (*gatewaytypes.Limit, error) {
return nil, errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}
func (p *provider) GetIngestionKeyLimits(_ context.Context, _ valuer.UUID, _ string) ([]gatewaytypes.Limit, error) {
return nil, errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}
func (p *provider) UpdateIngestionKeyLimit(_ context.Context, _ valuer.UUID, _ string, _ gatewaytypes.LimitConfig, _ []string) error {
return errors.New(errors.TypeUnsupported, gateway.ErrCodeGatewayUnsupported, "unsupported call")
}

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