Compare commits

..

74 Commits

Author SHA1 Message Date
nityanandagohain
f222e431ec chore: integration test for materialized 2026-03-09 21:44:06 +05:30
nityanandagohain
6487ba4ef6 fix: lint 2026-03-09 16:17:15 +05:30
nityanandagohain
95a1286d12 fix: lint 2026-03-09 16:10:22 +05:30
nityanandagohain
53a5bd0d54 Merge remote-tracking branch 'origin/main' into issue_3017 2026-03-09 15:56:21 +05:30
nityanandagohain
bfd7890144 chore: add integration tests 2026-03-09 15:56:05 +05:30
Srikanth Chekuri
d22543e03d Merge branch 'main' into issue_3017 2026-03-06 21:12:48 +05:30
Srikanth Chekuri
05b7a7df5a Merge branch 'main' into issue_3017 2026-03-06 11:58:46 +05:30
nityanandagohain
6af4b6c418 Merge remote-tracking branch 'origin/main' into issue_3017 2026-03-03 21:13:51 +05:30
nityanandagohain
0013bf937b fix: field_mapper 2026-03-03 20:40:49 +05:30
nityanandagohain
a1bca0006f fix: address comments 2026-03-03 15:08:36 +05:30
nityanandagohain
be12759524 fix: address comments 2026-03-02 22:48:03 +05:30
Srikanth Chekuri
e1d73862c7 Merge branch 'main' into issue_3017 2026-02-25 10:47:11 +05:30
nityanandagohain
108f03c7bd Merge remote-tracking branch 'origin/main' into issue_3017 2026-02-19 16:59:20 +05:30
nityanandagohain
ec5738032d fix: copy slice 2026-02-05 16:10:16 +05:30
nityanandagohain
4352c4de91 fix: final cleanup 2026-02-05 16:02:12 +05:30
nityanandagohain
6acbc7156d fix: add support for version in the evolutions 2026-02-05 15:24:55 +05:30
nityanandagohain
09f9a2d4f2 Merge remote-tracking branch 'origin/main' into issue_3017 2026-02-05 13:47:45 +05:30
nityanandagohain
6a5354df39 Merge remote-tracking branch 'origin/main' into issue_3017 2026-02-04 16:26:14 +05:30
nityanandagohain
ca9ff25314 fix: more fixes 2026-02-04 16:22:18 +05:30
nityanandagohain
07e66e8c24 Merge remote-tracking branch 'origin/main' into issue_3017 2026-02-04 11:25:24 +05:30
nityanandagohain
6283c6c26a fix: evolution metadata 2026-02-04 11:25:10 +05:30
nityanandagohain
3515e59a39 fix: minor refactoring and addressing comments 2026-02-03 10:48:51 +05:30
nityanandagohain
7756067914 fix: revert commented test 2026-02-03 10:17:13 +05:30
Nityananda Gohain
d3ef59cba7 Merge branch 'main' into issue_3017 2026-02-03 10:14:07 +05:30
nityanandagohain
81e33d59bb fix: address comments 2026-02-03 10:13:41 +05:30
nityanandagohain
a05957dc69 Merge remote-tracking branch 'origin/main' into issue_3017 2026-02-01 10:59:54 +05:30
nityanandagohain
24cf357b04 Merge remote-tracking branch 'origin/issue_3017' into issue_3017 2026-02-01 10:14:08 +05:30
nityanandagohain
91e4da28e6 fix: update conditionfor 2026-02-01 10:10:45 +05:30
Srikanth Chekuri
4cc727b7f8 Merge branch 'main' into issue_3017 2026-01-14 23:22:57 +05:30
nityanandagohain
9b24097a61 fix: edgecases 2026-01-14 00:20:33 +05:30
nityanandagohain
3a5d6b4493 fix: update query in adjust keys 2026-01-13 23:22:25 +05:30
nityanandagohain
d341f1f810 fix: minor cleanup 2026-01-13 21:52:21 +05:30
nityanandagohain
df1b47230a fix: add api 2026-01-13 01:48:21 +05:30
Nityananda Gohain
6261c9586f Merge branch 'main' into issue_3017 2026-01-13 01:47:01 +05:30
nityanandagohain
cda48874d2 fix: structural changes 2026-01-13 01:46:17 +05:30
nityanandagohain
277b6de266 fix: more changes 2026-01-12 19:43:09 +05:30
nityanandagohain
6f87ebe092 fix: fetch keys at the start 2026-01-12 18:00:29 +05:30
nityanandagohain
62c70715e0 Merge remote-tracking branch 'origin/main' into issue_3017 2026-01-12 16:27:52 +05:30
nityanandagohain
585a2b5282 fix: clean up evolution logic 2026-01-12 16:25:59 +05:30
nityanandagohain
6ad4c8ad8e fix: more changes 2026-01-09 23:34:44 +05:30
nityanandagohain
68df57965d fix: refactor code 2026-01-09 19:27:09 +05:30
nityanandagohain
d155cc6a10 fix: changes 2026-01-08 23:26:52 +05:30
Srikanth Chekuri
90a6902093 Merge branch 'main' into issue_3017 2026-01-08 18:43:57 +05:30
nityanandagohain
2bf92c9c2f fix: tests 2026-01-08 13:06:48 +05:30
nityanandagohain
aa2c1676b6 fix: tests 2026-01-08 12:59:13 +05:30
nityanandagohain
239c0f4e2e fix: merge conflicts 2026-01-08 12:51:58 +05:30
Srikanth Chekuri
97ecfdea23 Merge branch 'main' into issue_3017 2026-01-03 00:40:03 +05:30
nityanandagohain
6a02db8685 fix: address cursor comments 2026-01-02 15:53:41 +05:30
nityanandagohain
9f85dfb307 Merge remote-tracking branch 'origin/issue_3017' into issue_3017 2026-01-02 15:46:34 +05:30
nityanandagohain
ebc236857d fix: lint issues 2026-01-02 15:46:12 +05:30
Nityananda Gohain
0a1e252bb5 Merge branch 'main' into issue_3017 2026-01-02 15:34:05 +05:30
nityanandagohain
dd696bab13 fix: update the evolution metadata table 2026-01-02 15:32:05 +05:30
nityanandagohain
7f87103b30 fix: tests 2025-12-30 17:54:39 +05:30
nityanandagohain
726bd0ea7a Merge remote-tracking branch 'origin/main' into issue_3017 2025-12-30 15:34:37 +05:30
nityanandagohain
ab443c2d65 fix: make the evolution code reusable and propagte context properly 2025-12-30 15:33:58 +05:30
nityanandagohain
8be9a79d56 fix: use name from evolutions 2025-12-26 23:33:34 +05:30
nityanandagohain
471ad88971 fix: address changes and add tests 2025-12-26 23:25:43 +05:30
nityanandagohain
a5c46beeec Merge remote-tracking branch 'origin/main' into issue_3017 2025-12-23 18:21:40 +05:30
nityanandagohain
41f720950d fix: minor changes 2025-12-09 10:46:09 +07:00
nityanandagohain
d9bce4a3c6 fix: lint issues 2025-12-08 22:06:38 +07:00
nityanandagohain
a5ac40c33c fix: test 2025-12-08 21:40:30 +07:00
nityanandagohain
86b1366d4a fix: comments 2025-12-08 21:31:26 +07:00
nityanandagohain
eddb43a901 fix: aggregation 2025-12-08 21:30:07 +07:00
nityanandagohain
505cfe2314 fix: use orgId properly 2025-12-08 20:57:17 +07:00
nityanandagohain
6e54ee822a fix: use proper cache 2025-12-08 20:46:56 +07:00
nityanandagohain
d88cb8aba4 fix: minor changes 2025-12-08 20:18:34 +07:00
nityanandagohain
b823b2a1e1 fix: minor changes 2025-12-08 20:14:01 +07:00
nityanandagohain
7cfb7118a3 fix: update tests 2025-12-08 19:54:01 +07:00
nityanandagohain
59dfe7c0ed fix: remove goroutine 2025-12-08 19:11:22 +07:00
nityanandagohain
96b68b91c9 fix: update comment 2025-12-06 08:29:25 +05:30
nityanandagohain
be6ce8d4f1 fix: update fetch code 2025-12-06 08:27:53 +05:30
nityanandagohain
1fc58695c6 fix: tests 2025-12-06 06:47:29 +05:30
nityanandagohain
43450a187e Merge remote-tracking branch 'origin/main' into issue_3017 2025-12-06 05:15:26 +05:30
nityanandagohain
f4666d9c97 feat: time aware dynamic field mapper 2025-11-25 09:59:12 +05:30
182 changed files with 4387 additions and 4138 deletions

View File

@@ -13,6 +13,23 @@ on:
jobs:
tsc:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install
run: cd frontend && yarn install
- name: tsc
run: cd frontend && yarn tsc
tsc2:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||

View File

@@ -1,4 +1,4 @@
FROM golang:1.25-bookworm
FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -1,4 +1,4 @@
FROM node:22-bookworm AS build
FROM node:18-bullseye AS build
WORKDIR /opt/
COPY ./frontend/ ./
@@ -6,7 +6,7 @@ ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 yarn install
RUN CI=1 yarn build
FROM golang:1.25-bookworm
FROM golang:1.24-bullseye
ARG OS="linux"
ARG TARGETARCH

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.115.0
image: signoz/signoz:v0.114.1
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.115.0
image: signoz/signoz:v0.114.1
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.115.0}
image: signoz/signoz:${VERSION:-v0.114.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.115.0}
image: signoz/signoz:${VERSION:-v0.114.1}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -2108,15 +2108,6 @@ components:
token:
type: string
type: object
TypesPostableBulkInviteRequest:
properties:
invites:
items:
$ref: '#/components/schemas/TypesPostableInvite'
type: array
required:
- invites
type: object
TypesPostableForgotPassword:
properties:
email:
@@ -2205,8 +2196,6 @@ components:
type: string
role:
type: string
status:
type: string
updatedAt:
format: date-time
type: string
@@ -3549,7 +3538,9 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableBulkInviteRequest'
items:
$ref: '#/components/schemas/TypesPostableInvite'
type: array
responses:
"201":
description: Created

View File

@@ -100,7 +100,7 @@ This command:
3. Create a `.env` file in this directory:
```env
VITE_FRONTEND_API_ENDPOINT=http://localhost:8080
FRONTEND_API_ENDPOINT=http://localhost:8080
```
4. Start the development server:

View File

@@ -273,7 +273,6 @@ Options can be simple (direct link) or nested (with another question):
- Place logo files in `public/Logos/`
- Use SVG format
- Reference as `"/Logos/your-logo.svg"`
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo public/Logos/your-logo.svg` to minimise their size before committing.
### 4. Links

View File

@@ -1,216 +0,0 @@
# Query Range v5 — Design Principles & Architectural Contracts
## Purpose of This Document
This document defines the design principles, invariants, and architectural contracts of the Query Range v5 system. It is intended for the authors working on the querier and querier related parts codebase. Any change to the system must align with the principles described here. If a change would violate a principle, it must be flagged and discussed.
---
## Core Architectural Principle
**The user speaks OpenTelemetry. The storage speaks ClickHouse. The system translates between them. These two worlds must never leak into each other.**
Every design choice in Query Range flows from this separation. The user-facing API surface deals exclusively in `TelemetryFieldKey`: a representation of fields as they exist in the OpenTelemetry data model. The storage layer deals in ClickHouse column expressions, table names, and SQL fragments. The translation between them is mediated by a small set of composable abstractions with strict boundaries.
---
## The Central Type: `TelemetryFieldKey`
`TelemetryFieldKey` is the atomic unit of the entire query system. Every filter, aggregation, group-by, order-by, and select operation is expressed in terms of field keys. Understanding its design contracts is non-negotiable.
### Identity
A field key is identified by three dimensions:
- **Name** — the field name as the user knows it (`service.name`, `http.method`, `trace_id`)
- **FieldContext** — where the field lives in the OTel model (`resource`, `attribute`, `span`, `log`, `body`, `scope`, `event`, `metric`)
- **FieldDataType** — the data type (`string`, `bool`, `number`/`float64`/`int64`, array variants)
### Invariant: Same name does not mean same field
`status` as an attribute, `status` as a body JSON key, and `status` as a span-level field are **three different fields**. The context disambiguates. Code that resolves or compares field keys must always consider all three dimensions, never just the name.
### Invariant: Normalization happens once, at the boundary
`TelemetryFieldKey.Normalize()` is called during JSON unmarshaling. After normalization, the text representation `resource.service.name:string` and the programmatic construction `{Name: "service.name", FieldContext: Resource, FieldDataType: String}` are identical.
**Consequence:** Downstream code must never re-parse or re-normalize field keys. If you find yourself splitting on `.` or `:` deep in the pipeline, something is wrong — the normalization should have already happened.
### Invariant: The text format is `context.name:datatype`
Parsing rules (implemented in `GetFieldKeyFromKeyText` and `Normalize`):
1. Data type is extracted from the right, after the last `:`.
2. Field context is extracted from the left, before the first `.`, if it matches a known context prefix.
3. Everything remaining is the name.
Special case: `log.body.X` normalizes to `{FieldContext: body, Name: X}` — the `log.body.` prefix collapses because body fields under log are a nested context.
### Invariant: Historical aliases must be preserved
The `fieldContexts` map includes aliases (`tag` -> `attribute`, `spanfield` -> `span`, `logfield` -> `log`). These exist because older database entries use these names. Removing or changing these aliases will break existing saved queries and dashboard configurations.
---
## The Abstraction Stack
The query pipeline is built from four interfaces that compose vertically. Each layer has a single responsibility. Each layer depends only on the layers below it. This layering is intentional and must be preserved.
```
StatementBuilder <- Orchestrates everything into executable SQL
├── AggExprRewriter <- Rewrites aggregation expressions (maps field refs to columns)
├── ConditionBuilder <- Builds WHERE predicates (field + operator + value -> SQL)
└── FieldMapper <- Maps TelemetryFieldKey -> ClickHouse column expression
```
### FieldMapper
**Contract:** Given a `TelemetryFieldKey`, return a ClickHouse column expression that yields the value for that field when used in a SELECT.
**Principle:** This is the *only* place where field-to-column translation happens. No other layer should contain knowledge of how fields map to storage. If you need a column expression, go through the FieldMapper.
**Why:** The user says `http.request.method`. ClickHouse might store it as `attributes_string['http.request.method']`, or as a materialized column `` `attribute_string_http$$request$$method` ``, or via a JSON access path in a body column. This variation is entirely contained within the FieldMapper. Everything above it is storage-agnostic.
### ConditionBuilder
**Contract:** Given a field key, an operator, and a value, produce a valid SQL predicate for a WHERE clause.
**Dependency:** Uses FieldMapper for the left-hand side of the condition.
**Principle:** The ConditionBuilder owns all the complexity of operator semantics, i.e type casting, array operators (`hasAny`/`hasAll` vs `=`), existence checks, and negative operator behavior. This complexity must not leak upward into the StatementBuilder.
### AggExprRewriter
**Contract:** Given a user-facing aggregation expression like `sum(duration_nano)`, resolve field references within it and produce valid ClickHouse SQL.
**Dependency:** Uses FieldMapper to resolve field names within expressions.
**Principle:** Aggregation expressions are user-authored strings that contain field references. The rewriter parses them, identifies field references, resolves each through the FieldMapper, and reassembles the expression.
### StatementBuilder
**Contract:** Given a complete `QueryBuilderQuery`, a time range, and a request type, produces an executable SQL statement.
**Dependency:** Uses all three abstractions above.
**Principle:** This is the composition layer. It does not contain field mapping logic, condition building logic, or expression rewriting logic. It orchestrates the other abstractions. If you find storage-specific logic creeping into the StatementBuilder, push it down into the appropriate abstraction.
### Invariant: No layer skipping
The StatementBuilder must not call FieldMapper directly to build conditions, it goes through the ConditionBuilder. The AggExprRewriter must not hardcode column names, it goes through the FieldMapper. Skipping layers creates hidden coupling and makes the system fragile to storage changes.
---
## Design Decisions as Constraints
### Constraint: Formula evaluation happens in Go, not in ClickHouse
Formulas (`A + B`, `A / B`, `sqrt(A*A + B*B)`) are evaluated application-side by `FormulaEvaluator`, not via ClickHouse JOINs.
**Why this is a constraint, not just an implementation choice:** The original JOIN-based approach was abandoned because ClickHouse evaluates joins right-to-left, serializing execution unnecessarily. Running queries independently allows parallelism and caching of intermediate results. Any future optimization must not reintroduce the JOIN pattern without solving the serialization problem.
**Consequence:** Individual query results must be independently cacheable. Formula evaluation must handle label matching, timestamp alignment, and missing values without requiring the queries to coordinate at the SQL level.
### Constraint: Zero-defaulting is aggregation-dependent
Only additive/counting aggregations (`count`, `count_distinct`, `sum`, `rate`) default missing values to zero. Statistical aggregations (`avg`, `min`, `max`, percentiles) must show gaps.
**Why:** Absence of data has different meanings. No error requests in a time bucket means error count = 0. No requests at all means average latency is *unknown*, not 0. Conflating these is a correctness bug, not a display preference.
**Enforcement:** `GetQueriesSupportingZeroDefault` determines which queries can default to zero. The `FormulaEvaluator` consumes this via `canDefaultZero`. Changes to aggregation handling must preserve this distinction.
### Constraint: Existence semantics differ for positive vs negative operators
- **Positive operators** (`=`, `>`, `LIKE`, `IN`, etc.) implicitly assert field existence. `http.method = GET` means "the field exists AND equals GET".
- **Negative operators** (`!=`, `NOT IN`, `NOT LIKE`, etc.) do **not** add an existence check. `http.method != GET` includes records where the field doesn't exist at all.
**Why:** The user's intent with negative operators is ambiguous. Rather than guess, we take the broader interpretation. Users can add an explicit `EXISTS` filter if they want the narrower one. This is documented in `AddDefaultExistsFilter`.
**Consequence:** Any new operator must declare its existence behavior in `AddDefaultExistsFilter`. Do not add operators without considering this.
### Constraint: Post-processing functions operate on result sets, not in SQL
Functions like `cutOffMin`, `ewma`, `median`, `timeShift`, `fillZero`, `runningDiff`, and `cumulativeSum` are applied in Go on the returned time series, not pushed into ClickHouse SQL.
**Why:** These are sequential time-series transformations that require complete, ordered result sets. Pushing them into SQL would complicate query generation, prevent caching of raw results, and make the functions harder to test. They are applied via `ApplyFunctions` after query execution.
**Consequence:** New time-series transformation functions should follow this pattern i.e implement them as Go functions on `*TimeSeries`, not as SQL modifications.
### Constraint: The API surface rejects unknown fields with suggestions
All request types use custom `UnmarshalJSON` that calls `DisallowUnknownFields`. Unknown fields trigger error messages with Levenshtein-based suggestions ("did you mean: 'groupBy'?").
**Why:** Silent acceptance of unknown fields causes subtle bugs. A misspelled `groupBy` results in ungrouped data with no indication of what went wrong. Failing fast with suggestions turns errors into actionable feedback.
**Consequence:** Any new request type or query spec struct must implement custom unmarshaling with `UnmarshalJSONWithContext`. Do not use default `json.Unmarshal` for user-facing types.
### Constraint: Validation is context-sensitive to request type
What's valid depends on the `RequestType`. For aggregation requests (`time_series`, `scalar`, `distribution`), fields like `groupBy`, `aggregations`, `having`, and aggregation-referenced `orderBy` are validated. For non-aggregation requests (`raw`, `raw_stream`, `trace`), these fields are ignored.
**Why:** A raw log query doesn't have aggregations, so requiring `aggregations` would be wrong. But a time-series query without aggregations is meaningless. The validation rules are request-type-aware to avoid both false positives and false negatives.
**Consequence:** When adding new fields to query specs, consider which request types they apply to and gate validation accordingly.
---
## The Composite Query Model
### Structure
A `QueryRangeRequest` contains a `CompositeQuery` which holds `[]QueryEnvelope`. Each envelope is a discriminated union: a `Type` field determines how `Spec` is decoded.
### Invariant: Query names are unique within a composite query
Builder queries must have unique names. Formulas reference queries by name (`A`, `B`, `A.0`, `A.my_alias`). Duplicate names would make formula evaluation ambiguous.
### Invariant: Multi-aggregation uses indexed or aliased references
A single builder query can have multiple aggregations. They are accessed in formulas via:
- Index: `A.0`, `A.1` (zero-based)
- Alias: `A.total`, `A.error_count`
The default (just `A`) resolves to index 0. This is the formula evaluation contract and must be preserved.
### Invariant: Type-specific decoding through signal detection
Builder queries are decoded by first peeking at the `signal` field in the raw JSON, then unmarshaling into the appropriate generic type (`QueryBuilderQuery[TraceAggregation]`, `QueryBuilderQuery[LogAggregation]`, `QueryBuilderQuery[MetricAggregation]`). This two-pass decoding is intentional — it allows each signal to have its own aggregation schema while sharing the query structure.
---
## The Metadata Layer
### MetadataStore
The `MetadataStore` interface provides runtime field discovery and type resolution. It answers questions like "what fields exist for this signal?" and "what are the data types of field X?".
### Principle: Fields can be ambiguous until resolved
The same name can map to multiple `TelemetryFieldKey` variants (different contexts, different types). The metadata store returns *all* variants. Resolution to a single field happens during query building, using the query's signal and any explicit context/type hints from the user.
**Consequence:** Code that calls `GetKey` or `GetKeys` must handle multiple results. Do not assume a name maps to a single field.
### Principle: Materialized fields are a performance optimization, not a semantic distinction
A materialized field and its non-materialized equivalent represent the same logical field. The `Materialized` flag tells the FieldMapper to generate a simpler column expression. The user should never need to know whether a field is materialized.
### Principle: JSON body fields require access plans
Fields inside JSON body columns (`body.response.errors[].code`) need pre-computed `JSONAccessPlan` trees that encode the traversal path, including branching at array boundaries between `Array(JSON)` and `Array(Dynamic)` representations. These plans are computed during metadata resolution, not during query execution.
---
## Summary of Inviolable Rules
1. **User-facing types never contain ClickHouse column names or SQL fragments.**
2. **Field-to-column translation only happens in FieldMapper.**
3. **Normalization happens once at the API boundary, never deeper.**
4. **Historical aliases in fieldContexts and fieldDataTypes must not be removed.**
5. **Formula evaluation stays in Go — do not push it into ClickHouse JOINs.**
6. **Zero-defaulting is aggregation-type-dependent — do not universally default to zero.**
7. **Positive operators imply existence, negative operators do not.**
8. **Post-processing functions operate on Go result sets, not in SQL.**
9. **All user-facing types reject unknown JSON fields with suggestions.**
10. **Validation rules are gated by request type.**
11. **Query names must be unique within a composite query.**
12. **The four-layer abstraction stack (FieldMapper -> ConditionBuilder -> AggExprRewriter -> StatementBuilder) must not be bypassed or flattened.**

View File

@@ -176,7 +176,6 @@ func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource
typeables = append(typeables, register.MustGetTypeables()...)
}
typeables = append(typeables, provider.MustGetTypeables()...)
resources := make([]*authtypes.Resource, 0)
for _, typeable := range typeables {
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})

View File

@@ -170,7 +170,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}

View File

@@ -1,6 +1,6 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080/"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"

View File

@@ -39,7 +39,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>

Before

Width:  |  Height:  |  Size: 707 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 88" fill="none"><path fill="#ffd21e" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5"/><path fill="#ff9d0b" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0m-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0"/><path fill="#3a3b45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32Zm-23.55 0c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"/><path fill="#ff323d" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"/><path fill="#3a3b45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.6 8.6 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97" clip-rule="evenodd"/><path fill="#ff9d0b" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m-46.5 0a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m-6.69 11c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7 7 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.3 44.3 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48m0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85"/><path fill="#ffd21e" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9-3.24 5.08.68 8.01c3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12"/><path fill="#ff9d0b" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7 7 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.3 44.3 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48m0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.5 1.5 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85"/><path fill="#ffd21e" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44-1.58 4.17-6.8 7.33c-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12"/></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#76b900" viewBox="0 0 24 24"><title>NVIDIA</title><path d="M8.948 8.798v-1.43a7 7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851a3.7 3.7 0 0 1-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6 6 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964a6.5 6.5 0 0 1-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936"/></svg>

Before

Width:  |  Height:  |  Size: 865 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>

Before

Width:  |  Height:  |  Size: 685 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3.94 -1.44 438.62 432.87"><path fill="#e75225" d="M215.926 7.068c115.684.024 210.638 93.784 210.493 207.844-.148 115.793-94.713 208.252-212.912 208.169C97.95 423 4.52 329.143 4.601 213.221 4.68 99.867 99.833 7.044 215.926 7.068m-63.947 73.001c2.652 12.978.076 25.082-3.846 36.988-2.716 8.244-6.47 16.183-8.711 24.539-3.694 13.769-7.885 27.619-9.422 41.701-2.21 20.25 5.795 38.086 19.493 55.822L86.527 225.94c.11 1.978-.007 2.727.21 3.361 5.968 17.43 16.471 32.115 28.243 45.957 1.246 1.465 4.082 2.217 6.182 2.221 62.782.115 125.565.109 188.347.028 1.948-.003 4.546-.369 5.741-1.618 13.456-14.063 23.746-30.079 30.179-50.257l-66.658 12.976c4.397-8.567 9.417-16.1 12.302-24.377 9.869-28.315 5.779-55.69-8.387-81.509-11.368-20.72-21.854-41.349-16.183-66.32-12.005 11.786-16.615 26.79-19.541 42.253-2.882 15.23-4.58 30.684-6.811 46.136-.317-.467-.728-.811-.792-1.212-.258-1.621-.499-3.255-.587-4.893-1.355-25.31-6.328-49.696-16.823-72.987-6.178-13.71-12.99-27.727-6.622-44.081-4.31 2.259-8.205 4.505-10.997 7.711-8.333 9.569-11.779 21.062-12.666 33.645-.757 10.75-1.796 21.552-3.801 32.123-2.107 11.109-5.448 21.998-12.956 32.209-3.033-21.81-3.37-43.38-22.928-57.237m161.877 216.523H116.942v34.007h196.914zm-157.871 51.575c-.163 28.317 28.851 49.414 64.709 47.883 29.716-1.269 56.016-24.51 53.755-47.883z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 122.88 103.53"><path d="M5.47 0H117.4c3.01 0 5.47 2.46 5.47 5.47v92.58c0 3.01-2.46 5.47-5.47 5.47H5.47c-3.01 0-5.47-2.46-5.47-5.47V5.47C0 2.46 2.46 0 5.47 0m26.37 38.55 17.79 18.42 2.14 2.13-2.12 2.16-17.97 19.05-5.07-5 15.85-16.15L26.81 43.6zM94.1 79.41H54.69v-6.84H94.1zM38.19 9.83c3.19 0 5.78 2.59 5.78 5.78s-2.59 5.78-5.78 5.78-5.78-2.59-5.78-5.78S35 9.83 38.19 9.83m-19.24 0c3.19 0 5.78 2.59 5.78 5.78s-2.59 5.78-5.78 5.78-5.78-2.59-5.78-5.78 2.58-5.78 5.78-5.78M7.49 5.41H115.4c1.15 0 2.09.94 2.09 2.09v18.32H5.4V7.5c0-1.15.94-2.09 2.09-2.09" style="fill-rule:evenodd;clip-rule:evenodd;fill:#1668dc"/></svg>

Before

Width:  |  Height:  |  Size: 687 B

View File

@@ -2525,13 +2525,6 @@ export interface TypesPostableAcceptInviteDTO {
token?: string;
}
export interface TypesPostableBulkInviteRequestDTO {
/**
* @type array
*/
invites: TypesPostableInviteDTO[];
}
export interface TypesPostableForgotPasswordDTO {
/**
* @type string
@@ -2672,10 +2665,6 @@ export interface TypesUserDTO {
* @type string
*/
role?: string;
/**
* @type string
*/
status?: string;
/**
* @type string
* @format date-time

View File

@@ -41,7 +41,6 @@ import type {
TypesChangePasswordRequestDTO,
TypesPostableAcceptInviteDTO,
TypesPostableAPIKeyDTO,
TypesPostableBulkInviteRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
@@ -672,14 +671,14 @@ export const useAcceptInvite = <
* @summary Create bulk invite
*/
export const createBulkInvite = (
typesPostableBulkInviteRequestDTO: BodyType<TypesPostableBulkInviteRequestDTO>,
typesPostableInviteDTO: BodyType<TypesPostableInviteDTO[]>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/invite/bulk`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableBulkInviteRequestDTO,
data: typesPostableInviteDTO,
signal,
});
};
@@ -691,13 +690,13 @@ export const getCreateBulkInviteMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<TypesPostableInviteDTO[]> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<TypesPostableInviteDTO[]> },
TContext
> => {
const mutationKey = ['createBulkInvite'];
@@ -711,7 +710,7 @@ export const getCreateBulkInviteMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createBulkInvite>>,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> }
{ data: BodyType<TypesPostableInviteDTO[]> }
> = (props) => {
const { data } = props ?? {};
@@ -724,7 +723,7 @@ export const getCreateBulkInviteMutationOptions = <
export type CreateBulkInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof createBulkInvite>>
>;
export type CreateBulkInviteMutationBody = BodyType<TypesPostableBulkInviteRequestDTO>;
export type CreateBulkInviteMutationBody = BodyType<TypesPostableInviteDTO[]>;
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -737,13 +736,13 @@ export const useCreateBulkInvite = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<TypesPostableInviteDTO[]> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<TypesPostableInviteDTO[]> },
TContext
> => {
const mutationOptions = getCreateBulkInviteMutationOptions(options);

View File

@@ -94,13 +94,19 @@ export const interceptorRejected = async (
afterLogin(response.data.accessToken, response.data.refreshToken, true);
try {
const reResponse = await axios({
...value.config,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessToken}`,
const reResponse = await axios(
`${value.config.baseURL}${value.config.url?.substring(1)}`,
{
method: value.config.method,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessToken}`,
},
data: {
...JSON.parse(value.config.data || '{}'),
},
},
});
);
return await Promise.resolve(reResponse);
} catch (error) {

View File

@@ -14,7 +14,6 @@ import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import HavingFilter from './HavingFilter/HavingFilter';
import { buildDefaultLegendFromGroupBy } from './utils';
import './QueryAddOns.styles.scss';
@@ -251,33 +250,12 @@ function QueryAddOns({
}, [panelType, isListViewPanel, query, showReduceTo]);
const handleOptionClick = (e: RadioChangeEvent): void => {
const clickedAddOn = e.target.value as AddOn;
const isAlreadySelected = selectedViews.some(
(view) => view.key === clickedAddOn.key,
);
if (isAlreadySelected) {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== clickedAddOn.key),
if (selectedViews.find((view) => view.key === e.target.value.key)) {
setSelectedViews(
selectedViews.filter((view) => view.key !== e.target.value.key),
);
} else {
// When enabling Legend format for the first time with an empty legend
// and existing group-by keys, prefill the legend using all group-by keys.
// This keeps existing custom legends intact and only helps seed a sensible default.
if (
clickedAddOn.key === ADD_ONS_KEYS.LEGEND_FORMAT &&
isEmpty(query?.legend) &&
Array.isArray(query.groupBy) &&
query.groupBy.length > 0
) {
const defaultLegend = buildDefaultLegendFromGroupBy(query.groupBy);
if (defaultLegend) {
handleChangeQueryLegend(defaultLegend);
}
}
setSelectedViews((prev) => [...prev, clickedAddOn]);
setSelectedViews([...selectedViews, e.target.value]);
}
};
@@ -310,9 +288,12 @@ function QueryAddOns({
[handleSetQueryData, index, query],
);
const handleRemoveView = useCallback((key: string): void => {
setSelectedViews((prev) => prev.filter((view) => view.key !== key));
}, []);
const handleRemoveView = useCallback(
(key: string): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== key));
},
[selectedViews],
);
const handleChangeQueryLegend = useCallback(
(value: string) => {
@@ -398,8 +379,8 @@ function QueryAddOns({
<div className="input">
<HavingFilter
onClose={(): void => {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== 'having'),
setSelectedViews(
selectedViews.filter((view) => view.key !== 'having'),
);
}}
onChange={handleChangeHaving}
@@ -418,9 +399,7 @@ function QueryAddOns({
initialValue={query?.limit ?? undefined}
placeholder="Enter limit"
onClose={(): void => {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== 'limit'),
);
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
}}
closeIcon={<ChevronUp size={16} />}
/>
@@ -503,8 +482,8 @@ function QueryAddOns({
onChange={handleChangeQueryLegend}
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
onClose={(): void => {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== 'legend_format'),
setSelectedViews(
selectedViews.filter((view) => view.key !== 'legend_format'),
);
}}
closeIcon={<ChevronUp size={16} />}

View File

@@ -1,16 +0,0 @@
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const buildDefaultLegendFromGroupBy = (
groupBy: IBuilderQuery['groupBy'],
): string | null => {
const segments = groupBy
.map((item) => item?.key)
.filter((key): key is string => Boolean(key))
.map((key) => `${key} = {{${key}}}`);
if (segments.length === 0) {
return null;
}
return segments.join(', ');
};

View File

@@ -275,59 +275,4 @@ describe('QueryAddOns', () => {
});
});
});
it('auto-generates legend from all groupBy keys when enabling Legend format with empty legend', async () => {
const user = userEvent.setup();
const query = baseQuery({
groupBy: [{ key: 'service.name' }, { key: 'operation' }],
});
render(
<QueryAddOns
query={query}
version="v5"
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
isForTraceOperator={false}
/>,
);
const legendTab = screen.getByTestId('query-add-on-legend_format');
await user.click(legendTab);
expect(mockHandleChangeQueryData).toHaveBeenCalledWith(
'legend',
'service.name = {{service.name}}, operation = {{operation}}',
);
});
it('does not override existing legend when enabling Legend format', async () => {
const user = userEvent.setup();
const query = baseQuery({
legend: 'existing legend',
groupBy: [{ key: 'service.name' }],
});
render(
<QueryAddOns
query={query}
version="v5"
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
isForTraceOperator={false}
/>,
);
const legendTab = screen.getByTestId('query-add-on-legend_format');
await user.click(legendTab);
expect(mockHandleChangeQueryData).not.toHaveBeenCalledWith(
'legend',
expect.anything(),
);
});
});

View File

@@ -361,58 +361,6 @@ describe('CheckboxFilter - User Flows', () => {
expect(filtersForServiceName).toHaveLength(0);
});
it('should match filter when query uses resource. prefix (resource.service.name matches service.name)', async () => {
// Filter config uses unprefixed key (service.name)
// Query has filter with resource. prefix (resource.service.name)
// Checkbox should recognize the match and show checked state
mockUseQueryBuilder.mockReturnValue({
lastUsedQuery: 0,
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: {
key: 'resource.service.name',
dataType: DataTypes.String,
type: 'resource',
},
op: 'in',
value: [OTEL_DEMO],
},
],
op: 'AND',
},
},
],
},
},
redirectWithQueryBuilderData: jest.fn(),
} as any);
const mockFilter = createMockFilter({ defaultOpen: false });
render(
<CheckboxFilter
filter={mockFilter}
source={QuickFiltersSource.LOGS_EXPLORER}
/>,
);
// Filter should auto-open because it has active filters (key match via prefix stripping)
await waitFor(() => {
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
});
// otel-demo should be checked (filter uses resource.service.name IN [otel-demo])
// Checked items are sorted to the top, so otel-demo is first
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes[0]).toBeChecked();
expect(screen.getByText(OTEL_DEMO)).toBeInTheDocument();
});
it('should extend an existing IN filter when checking an additional value', async () => {
const redirectWithQueryBuilderData = jest.fn();

View File

@@ -18,7 +18,7 @@ import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isFunction } from 'lodash-es';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
@@ -26,7 +26,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
import { isKeyMatch } from './utils';
import './Checkbox.styles.scss';
@@ -85,7 +84,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
currentQuery.builder.queryData?.[
activeQueryIndex
]?.filters?.items?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
),
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
);
@@ -190,7 +189,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
);
if (filterSync) {
@@ -237,7 +236,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
(currentQuery?.builder?.queryData?.[
activeQueryIndex
]?.filters?.items?.filter((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
)?.length || 0) > 1,
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
@@ -281,7 +280,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
items:
idx === activeQueryIndex
? item.filters?.items?.filter(
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
) || []
: [...(item.filters?.items || [])],
op: item.filters?.op || 'AND',
@@ -314,7 +313,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
: 'Only'
: 'Only';
query.filters.items = query.filters.items.filter(
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
@@ -336,13 +335,13 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
} else if (query?.filters?.items) {
if (
query.filters?.items?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
isEqual(item.key?.key, filter.attributeKey.key),
)
) {
// if there is already a running filter for the current attribute key then
// we split the cases by which particular operator is present right now!
const currentFilter = query.filters?.items?.find((q) =>
isKeyMatch(q.key?.key, filter.attributeKey.key),
isEqual(q.key?.key, filter.attributeKey.key),
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
@@ -357,7 +356,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -369,7 +368,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -386,11 +385,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -399,7 +398,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
} else {
// if not an array remove the whole thing altogether!
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
}
@@ -415,7 +414,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -427,7 +426,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -442,7 +441,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
@@ -452,7 +451,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
} else {
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -470,7 +469,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
}
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
}
@@ -483,14 +482,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
} else if (!checked) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
break;
@@ -502,14 +501,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
} else if (checked) {
query.filters.items = query.filters.items.filter(
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
}
break;

View File

@@ -1,41 +0,0 @@
/**
* These prefixes are added to attribute keys based on their context.
*/
export const FIELD_CONTEXT_PREFIXES = [
'metric',
'log',
'span',
'trace',
'resource',
'scope',
'attribute',
'event',
'body',
];
/**
* Removes the field context prefix from a key to get the base key name.
* Example: 'resource.service.name' -> 'service.name'
* Example: 'attribute.http.method' -> 'http.method'
*/
export function getKeyWithoutPrefix(key: string | undefined): string {
if (!key) {
return '';
}
const prefixPattern = new RegExp(
`^(${FIELD_CONTEXT_PREFIXES.join('|')})\\.`,
'i',
);
return key.replace(prefixPattern, '').trim();
}
/**
* Compares two keys by their base name (without prefix).
* This ensures that 'service.name' matches 'resource.service.name'
*/
export function isKeyMatch(
itemKey: string | undefined,
filterKey: string | undefined,
): boolean {
return getKeyWithoutPrefix(itemKey) === getKeyWithoutPrefix(filterKey);
}

View File

@@ -55,5 +55,4 @@ export enum QueryParams {
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
yAxisUnit = 'yAxisUnit',
}

View File

@@ -1,3 +0,0 @@
export const DASHBOARD_CACHE_TIME = 30_000;
// keep it low or zero, otherwise, when enabled auto-refresh, this causes OOM due to accumulated queries in cache
export const DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED = 0;

View File

@@ -96,7 +96,4 @@ export const REACT_QUERY_KEY = {
// Span Percentiles Query Keys
GET_SPAN_PERCENTILES: 'GET_SPAN_PERCENTILES',
// Dashboard Grid Card Query Keys
DASHBOARD_GRID_CARD_QUERY_RANGE: 'DASHBOARD_GRID_CARD_QUERY_RANGE',
} as const;

View File

@@ -16,8 +16,8 @@ export const PagerInitialConfig: Partial<PagerChannel> = {
client: 'SigNoz Alert Manager',
client_url: 'https://enter-signoz-host-n-port-here/alerts',
details: JSON.stringify({
firing: `{{ .Alerts.Firing | toJson }}`,
resolved: `{{ .Alerts.Resolved | toJson }}`,
firing: `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
resolved: `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
num_firing: '{{ .Alerts.Firing | len }}',
num_resolved: '{{ .Alerts.Resolved | len }}',
}),

View File

@@ -193,6 +193,7 @@ describe('Dashboard landing page actions header tests', () => {
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
dashboardId: '4',
layouts: [],
panelMap: {},
setPanelMap: jest.fn(),
@@ -204,6 +205,8 @@ describe('Dashboard landing page actions header tests', () => {
updateLocalStorageDashboardVariables: jest.fn(),
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: jest.fn(),
selectedRowWidgetId: null,
setSelectedRowWidgetId: jest.fn(),
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: jest.fn(),

View File

@@ -78,6 +78,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
isDashboardLocked,
setSelectedDashboard,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
handleDashboardLockToggle,
} = useDashboard();
@@ -145,6 +146,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,

View File

@@ -13,10 +13,6 @@ import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isRetryableError as checkIfRetryableError } from 'utils/errorUtils';
import {
DASHBOARD_CACHE_TIME,
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from '../../../constants/queryCacheTime';
import SelectVariableInput from './SelectVariableInput';
import { useDashboardVariableSelectHelper } from './useDashboardVariableSelectHelper';
import {
@@ -105,10 +101,9 @@ function DynamicVariableInput({
return dynamicVars || 'no_dynamic_variables';
}, [existingVariables]);
const { maxTime, minTime, isAutoRefreshDisabled } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const {
variableFetchCycleId,
@@ -237,9 +232,6 @@ function DynamicVariableInput({
!!variableData.dynamicVariablesSource &&
!!variableData.dynamicVariablesAttribute &&
(isVariableFetching || (isVariableSettled && hasVariableFetchedOnce)),
cacheTime: isAutoRefreshDisabled
? DASHBOARD_CACHE_TIME
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
queryFn: ({ signal }) =>
getFieldValues(
variableData.dynamicVariablesSource?.toLowerCase() === 'all telemetry'

View File

@@ -11,10 +11,6 @@ import { AppState } from 'store/reducers';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
DASHBOARD_CACHE_TIME,
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from '../../../constants/queryCacheTime';
import { variablePropsToPayloadVariables } from '../utils';
import SelectVariableInput from './SelectVariableInput';
import { useDashboardVariableSelectHelper } from './useDashboardVariableSelectHelper';
@@ -37,10 +33,9 @@ function QueryVariableInput({
);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
const { maxTime, minTime, isAutoRefreshDisabled } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const {
variableFetchCycleId,
@@ -202,9 +197,6 @@ function QueryVariableInput({
signal,
),
refetchOnWindowFocus: false,
cacheTime: isAutoRefreshDisabled
? DASHBOARD_CACHE_TIME
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
onSuccess: (response) => {
getOptions(response.payload);
settleVariableFetch(variableData.name, 'complete');

View File

@@ -67,18 +67,17 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
const oldVariables = prev?.data.variables;
// this is added to handle case where we have two different
// schemas for variable response
const updatedVariables = { ...oldVariables };
if (updatedVariables?.[id]) {
updatedVariables[id] = {
...updatedVariables[id],
if (oldVariables?.[id]) {
oldVariables[id] = {
...oldVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (updatedVariables?.[name]) {
updatedVariables[name] = {
...updatedVariables[name],
if (oldVariables?.[name]) {
oldVariables[name] = {
...oldVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
@@ -88,7 +87,9 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
...prev,
data: {
...prev?.data,
variables: updatedVariables,
variables: {
...oldVariables,
},
},
};
}

View File

@@ -6,6 +6,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import ContextMenu from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -61,7 +62,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -11,6 +11,7 @@ import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { AlignedData } from 'uplot';
import { PanelMode } from '../types';
@@ -43,7 +44,7 @@ export function prepareBarPanelConfig({
currentQuery: Query;
onClick: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
@@ -75,17 +76,13 @@ export function prepareBarPanelConfig({
stepInterval: minStepInterval,
});
if (!(apiResponse && apiResponse?.data?.result)) {
// if no data, return the builder without adding any series
return builder;
}
if (widget.stackedBarChart) {
const seriesCount = (apiResponse.data.result.length ?? 0) + 1; // +1 for 1-based uPlot series indices
const seriesCount = (apiResponse?.data?.result?.length ?? 0) + 1; // +1 for 1-based uPlot series indices
builder.setBands(getInitialStackedBands(seriesCount));
}
apiResponse.data.result.forEach((series) => {
const seriesList: QueryData[] = apiResponse?.data?.result || [];
seriesList.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query

View File

@@ -6,6 +6,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import Histogram from '../../charts/Histogram/Histogram';
@@ -38,7 +39,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return prepareHistogramPanelConfig({
widget,
isDarkMode,
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
panelMode,
});
}, [widget, isDarkMode, queryResponse?.data?.payload, panelMode]);
@@ -48,7 +49,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return [];
}
return prepareHistogramPanelData({
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
bucketWidth: widget?.bucketWidth,
bucketCount: widget?.bucketCount,
mergeAllActiveQueries: widget?.mergeAllActiveQueries,

View File

@@ -149,7 +149,7 @@ export function prepareHistogramPanelConfig({
isDarkMode,
}: {
widget: Widgets;
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
panelMode: PanelMode;
isDarkMode: boolean;
}): UPlotConfigBuilder {
@@ -204,7 +204,7 @@ export function prepareHistogramPanelConfig({
fillColor: '#4E74F8',
isDarkMode,
});
} else if (apiResponse && apiResponse?.data?.result) {
} else {
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,

View File

@@ -9,6 +9,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -67,7 +68,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -68,12 +68,11 @@ export const prepareUPlotConfig = ({
currentQuery: Query;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
maxTimeScale?: number;
// eslint-disable-next-line sonarjs/cognitive-complexity
}): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
@@ -101,12 +100,7 @@ export const prepareUPlotConfig = ({
stepInterval: minStepInterval,
});
if (!(apiResponse && apiResponse.data.result)) {
// if no data, return the builder without adding any series
return builder;
}
apiResponse.data.result.forEach((series) => {
apiResponse.data?.result?.forEach((series) => {
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
const baseLabelName = getLabelName(
series.metric,

View File

@@ -18,7 +18,7 @@ import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
id: string;
thresholds?: ThresholdProps[];
apiResponse?: MetricRangePayloadProps;
apiResponse: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;

View File

@@ -19,6 +19,7 @@ export default function DashboardEmptyState(): JSX.Element {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
} = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
@@ -41,6 +42,7 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,

View File

@@ -25,11 +25,6 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import {
DASHBOARD_CACHE_TIME,
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from '../../../constants/queryCacheTime';
import { REACT_QUERY_KEY } from '../../../constants/reactQueryKeys';
import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants';
import { GridCardGraphProps } from './types';
@@ -73,12 +68,10 @@ function GridCardGraph({
setDashboardQueryRangeCalled,
} = useDashboard();
const {
minTime,
maxTime,
selectedTime: globalSelectedInterval,
isAutoRefreshDisabled,
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
@@ -217,10 +210,8 @@ function GridCardGraph({
version || DEFAULT_ENTITY_VERSION,
{
queryKey: [
REACT_QUERY_KEY.DASHBOARD_GRID_CARD_QUERY_RANGE,
maxTime,
minTime,
isAutoRefreshDisabled,
globalSelectedInterval,
widget?.query,
widget?.panelTypes,
@@ -250,9 +241,6 @@ function GridCardGraph({
return failureCount < 2;
},
keepPreviousData: true,
cacheTime: isAutoRefreshDisabled
? DASHBOARD_CACHE_TIME
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
enabled: queryEnabledCondition,
refetchOnMount: false,
onError: (error) => {

View File

@@ -71,6 +71,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
} = useDashboard();
@@ -194,6 +195,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout) {
setLayouts(sortLayout(updatedDashboard.data.data.layout));

View File

@@ -5,7 +5,6 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -38,6 +37,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
setSelectedRowWidgetId,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
@@ -81,12 +81,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
icon={<Plus size={14} />}
onClick={(): void => {
// TODO: @AshwinBhatkal Simplify this check in cleanup of https://github.com/SigNoz/engineering-pod/issues/3953
if (!selectedDashboard?.id) {
return;
}
setSelectedRowWidgetId(selectedDashboard.id, id);
setSelectedRowWidgetId(id);
handleToggleDashboardSlider(true);
}}
>

View File

@@ -39,7 +39,6 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQueryData from 'hooks/useUrlQueryData';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { isEmpty, isUndefined } from 'lodash-es';
import LiveLogs from 'pages/LiveLogs';
import { UpdateTimeInterval } from 'store/actions';
@@ -60,7 +59,6 @@ import LogsActionsContainer from './LogsActionsContainer';
import './LogsExplorerViews.styles.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
function LogsExplorerViewsContainer({
setIsLoadingQueries,
listQueryKeyRef,
@@ -111,7 +109,7 @@ function LogsExplorerViewsContainer({
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [
stagedQuery,
@@ -369,6 +367,10 @@ function LogsExplorerViewsContainer({
orderBy,
]);
const onUnitChangeHandler = useCallback((value: string): void => {
setYAxisUnit(value);
}, []);
const chartData = useMemo(() => {
if (!stagedQuery) {
return [];
@@ -486,7 +488,10 @@ function LogsExplorerViewsContainer({
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</div>
<TimeSeriesView
isLoading={isLoading || isFetching}

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -11,7 +11,6 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsF
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { AppState } from 'store/reducers';
@@ -23,13 +22,14 @@ import { GlobalReducer } from 'types/reducer/globalTime';
function TimeSeries(): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
@@ -112,6 +112,10 @@ function TimeSeries(): JSX.Element {
[data, isValidToConvertToMs],
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
const hasMetricSelected = useMemo(
() => currentQuery.builder.queryData.some((q) => q.aggregateAttribute?.key),
[currentQuery],
@@ -119,7 +123,7 @@ function TimeSeries(): JSX.Element {
return (
<div className="meter-time-series-container">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<div className="time-series-container">
{!hasMetricSelected && <EmptyMetricsSearch />}
{hasMetricSelected &&

View File

@@ -34,10 +34,6 @@ import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
clearSelectedRowWidgetId,
getSelectedRowWidgetId,
} from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import {
getNextWidgets,
getPreviousWidgets,
@@ -90,6 +86,8 @@ function NewWidget({
selectedDashboard,
setSelectedDashboard,
setToScrollWidgetId,
selectedRowWidgetId,
setSelectedRowWidgetId,
columnWidths,
} = useDashboard();
@@ -452,8 +450,6 @@ function NewWidget({
const widgetId = query.get('widgetId') || '';
let updatedLayout = selectedDashboard.data.layout || [];
const selectedRowWidgetId = getSelectedRowWidgetId(dashboardId);
if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
updatedLayout = [...updatedLayout, newLayoutItem];
@@ -558,6 +554,7 @@ function NewWidget({
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
setSelectedDashboard(updatedDashboard.data);
setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({
@@ -569,6 +566,7 @@ function NewWidget({
selectedDashboard,
query,
isNewDashboard,
selectedRowWidgetId,
afterWidgets,
selectedWidget,
selectedTime.enum,
@@ -579,6 +577,7 @@ function NewWidget({
widgets,
setSelectedDashboard,
setToScrollWidgetId,
setSelectedRowWidgetId,
safeNavigate,
dashboardId,
]);
@@ -682,10 +681,6 @@ function NewWidget({
* on mount here with the currentQuery in the begining itself
*/
setSupersetQuery(currentQuery);
return (): void => {
clearSelectedRowWidgetId(dashboardId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@@ -11,7 +11,7 @@ import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
import { InviteMemberFormValues } from 'container/OrganizationSettings/utils';
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
import history from 'lib/history';
import { UserPlus } from 'lucide-react';
import { useAppContext } from 'providers/App/App';

View File

@@ -12,7 +12,7 @@ import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { InviteTeamMembersProps } from 'container/OrganizationSettings/utils';
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';

View File

@@ -619,11 +619,6 @@ function OnboardingAddDataSource(): JSX.Element {
);
};
const progressText = `Get Started (${Math.min(
currentStep + 1,
setupStepItems.length,
)}/${setupStepItems.length})`;
return (
<div className="onboarding-v2">
<Layout>
@@ -644,7 +639,7 @@ function OnboardingAddDataSource(): JSX.Element {
history.push(ROUTES.HOME);
}}
/>
<Typography.Text>{progressText}</Typography.Text>
<Typography.Text>Get Started (2/4)</Typography.Text>
</div>
<div className="header-right-section">

View File

@@ -611,34 +611,6 @@
]
}
},
{
"dataSource": "deno",
"label": "Deno",
"imgUrl": "/Logos/deno.svg",
"tags": [
"apm/traces",
"logs",
"metrics"
],
"module": "apm",
"relatedSearchKeywords": [
"deno",
"deno monitoring",
"deno observability",
"javascript",
"js",
"ts",
"typescript",
"opentelemetry deno",
"observability",
"metrics",
"logs",
"traces",
"tracing"
],
"id": "deno",
"link": "/docs/instrumentation/opentelemetry-deno/"
},
{
"dataSource": "javascript",
"label": "JavaScript",
@@ -1856,33 +1828,15 @@
],
"module": "logs",
"relatedSearchKeywords": [
"apache",
"application log file collection",
"c#",
"c++",
"collect",
"collect logs from file",
"collect logs from log file",
"cpp",
"csharp",
"custom log file monitoring",
"deno",
"django",
"dotnet",
"elixir",
"express",
"file",
"file based logging",
"filebeat alternative",
"flask",
"from-log-file",
"go",
"golang",
"java",
"javascript",
"js",
"kotlin",
"linux",
"log",
"log file ingestion",
"log file to signoz",
@@ -1890,29 +1844,7 @@
"log tailing with otel",
"logging",
"logs",
"macos",
"mongodb",
"mysql",
"nestjs",
"nextjs",
"nginx",
"otel file logs",
"php",
"postgres",
"postgresql",
"python",
"rails",
"react",
"redis",
"ruby",
"rust",
"scala",
"spring",
"swift",
"ts",
"typescript",
"vue",
"windows"
"otel file logs"
],
"link": "/docs/userguide/collect_logs_from_file/"
},
@@ -5276,7 +5208,7 @@
{
"dataSource": "prometheus-metrics",
"label": "Prometheus Metrics",
"imgUrl": "/Logos/prometheus.svg",
"imgUrl": "/Logos/other-metrics.svg",
"tags": [
"metrics",
"infrastructure monitoring"
@@ -5877,250 +5809,5 @@
}
]
}
},
{
"dataSource": "rust-metrics",
"label": "Rust Metrics",
"imgUrl": "/Logos/rust.svg",
"tags": [
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"metrics",
"opentelemetry rust",
"otel rust",
"rust",
"rust metrics",
"rust monitoring",
"rust observability"
],
"id": "rust-metrics",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
},
{
"key": "windows",
"label": "Windows",
"imgUrl": "/Logos/windows.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
},
{
"key": "docker",
"label": "Docker",
"imgUrl": "/Logos/docker.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-rust/"
}
]
}
},
{
"dataSource": "ruby-metrics",
"label": "Ruby Metrics",
"imgUrl": "/Logos/ruby-on-rails.svg",
"tags": [
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"metrics",
"opentelemetry ruby",
"otel ruby",
"ruby",
"ruby metrics",
"ruby monitoring",
"ruby observability",
"ruby on rails metrics"
],
"id": "ruby-metrics",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/",
"question": {
"desc": "What is your Environment?",
"type": "select",
"entityID": "environment",
"options": [
{
"key": "vm",
"label": "VM",
"imgUrl": "/Logos/vm.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
},
{
"key": "k8s",
"label": "Kubernetes",
"imgUrl": "/Logos/kubernetes.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
},
{
"key": "windows",
"label": "Windows",
"imgUrl": "/Logos/windows.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
},
{
"key": "docker",
"label": "Docker",
"imgUrl": "/Logos/docker.svg",
"link": "/docs/metrics-management/send-metrics/applications/opentelemetry-ruby/"
}
]
}
},
{
"dataSource": "statsd",
"label": "StatsD",
"imgUrl": "/Logos/statsd.svg",
"tags": [
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"metrics",
"statsd",
"statsd metrics",
"statsd monitoring",
"statsd observability"
],
"id": "statsd",
"link": "/docs/userguide/opentelemetry-statsd/"
},
{
"dataSource": "nvidia-dcgm",
"label": "Nvidia DCGM",
"imgUrl": "/Logos/nvidia-dcgm.svg",
"tags": [
"infrastructure monitoring"
],
"module": "infra-monitoring-hosts",
"relatedSearchKeywords": [
"dcgm",
"gpu monitoring",
"infrastructure monitoring",
"nvidia",
"nvidia dcgm",
"nvidia metrics"
],
"id": "nvidia-dcgm",
"link": "/docs/metrics-management/nvidia-dcgm-metrics/"
},
{
"dataSource": "slurm",
"label": "Slurm",
"imgUrl": "/Logos/slurm.svg",
"tags": [
"infrastructure monitoring"
],
"module": "infra-monitoring-hosts",
"relatedSearchKeywords": [
"infrastructure monitoring",
"slurm",
"slurm cluster monitoring",
"slurm metrics"
],
"id": "slurm",
"link": "/docs/metrics-management/slurm-metrics/"
},
{
"dataSource": "ollama-monitoring",
"label": "Ollama",
"imgUrl": "/Logos/ollama.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"llm",
"llm monitoring",
"monitoring",
"observability",
"ollama",
"ollama monitoring",
"otel ollama",
"traces",
"tracing"
],
"id": "ollama-monitoring",
"link": "/docs/ollama-monitoring/"
},
{
"dataSource": "haystack-monitoring",
"label": "Haystack",
"imgUrl": "/Logos/haystack.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"haystack",
"haystack monitoring",
"llm",
"llm monitoring",
"monitoring",
"observability",
"otel haystack",
"traces",
"tracing"
],
"id": "haystack-monitoring",
"link": "/docs/haystack-monitoring/"
},
{
"dataSource": "openrouter-observability",
"label": "OpenRouter",
"imgUrl": "/Logos/openrouter.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"llm",
"llm monitoring",
"monitoring",
"observability",
"openrouter",
"openrouter monitoring",
"otel openrouter",
"traces",
"tracing"
],
"id": "openrouter-observability",
"link": "/docs/openrouter-observability/"
},
{
"dataSource": "huggingface-observability",
"label": "Hugging Face",
"imgUrl": "/Logos/huggingface.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"hugging face monitoring",
"huggingface",
"llm",
"llm monitoring",
"monitoring",
"observability",
"otel huggingface",
"traces",
"tracing"
],
"id": "huggingface-observability",
"link": "/docs/huggingface-observability/"
}
]

View File

@@ -0,0 +1,32 @@
import { gold } from '@ant-design/colors';
import { ExclamationCircleTwoTone } from '@ant-design/icons';
import { Space, Typography } from 'antd';
function DeleteMembersDetails({
name,
}: DeleteMembersDetailsProps): JSX.Element {
return (
<div>
<Space direction="horizontal" size="middle" align="start">
<ExclamationCircleTwoTone
twoToneColor={[gold[6], '#1f1f1f']}
style={{
fontSize: '1.4rem',
}}
/>
<Space direction="vertical">
<Typography>Are you sure you want to delete {name}</Typography>
<Typography>
This will remove all access from dashboards and other features in SigNoz
</Typography>
</Space>
</Space>
</div>
);
}
interface DeleteMembersDetailsProps {
name: string;
}
export default DeleteMembersDetails;

View File

@@ -0,0 +1,167 @@
import {
ChangeEventHandler,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import { CopyOutlined } from '@ant-design/icons';
import { Button, Input, Select, Space, Tooltip } from 'antd';
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { InputGroup, SelectDrawer, Title } from './styles';
const { Option } = Select;
function EditMembersDetails({
emailAddress,
name,
role,
setEmailAddress,
setName,
setRole,
id,
}: EditMembersDetailsProps): JSX.Element {
const [passwordLink, setPasswordLink] = useState<string>('');
const { t } = useTranslation(['common']);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [state, copyToClipboard] = useCopyToClipboard();
const getPasswordLink = (token: string): string =>
`${window.location.origin}${ROUTES.PASSWORD_RESET}?token=${token}`;
const onChangeHandler = useCallback(
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
setFunc(value);
},
[],
);
const { notifications } = useNotifications();
useEffect(() => {
if (state.error) {
notifications.error({
message: t('something_went_wrong'),
});
}
if (state.value) {
notifications.success({
message: t('success'),
});
}
}, [state.error, state.value, t, notifications]);
const onPasswordChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
setPasswordLink(event.target.value);
},
[],
);
const onGeneratePasswordHandler = async (): Promise<void> => {
try {
setIsLoading(true);
const response = await getResetPasswordToken({
userId: id || '',
});
setPasswordLink(getPasswordLink(response.data.token));
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
};
return (
<Space direction="vertical" size="large">
<Space direction="horizontal">
<Title>Email address</Title>
<Input
placeholder="john@signoz.io"
readOnly
onChange={(event): void =>
onChangeHandler(setEmailAddress, event.target.value)
}
disabled={isLoading}
value={emailAddress}
/>
</Space>
<Space direction="horizontal">
<Title>Name (optional)</Title>
<Input
placeholder="John"
onChange={(event): void => onChangeHandler(setName, event.target.value)}
value={name}
disabled={isLoading}
/>
</Space>
<Space direction="horizontal">
<Title>Role</Title>
<SelectDrawer
value={role}
onSelect={(value: unknown): void => {
if (typeof value === 'string') {
setRole(value as ROLES);
}
}}
disabled={isLoading}
>
<Option value="ADMIN">ADMIN</Option>
<Option value="VIEWER">VIEWER</Option>
<Option value="EDITOR">EDITOR</Option>
</SelectDrawer>
</Space>
<Button
loading={isLoading}
disabled={isLoading}
onClick={onGeneratePasswordHandler}
type="primary"
>
Generate Reset Password link
</Button>
{passwordLink && (
<InputGroup>
<Input
style={{ width: '100%' }}
defaultValue="git@github.com:ant-design/ant-design.git"
onChange={onPasswordChangeHandler}
value={passwordLink}
disabled={isLoading}
/>
<Tooltip title="COPY LINK">
<Button
icon={<CopyOutlined />}
onClick={(): void => copyToClipboard(passwordLink)}
/>
</Tooltip>
</InputGroup>
)}
</Space>
);
}
interface EditMembersDetailsProps {
emailAddress: string;
name: string;
role: ROLES;
setEmailAddress: Dispatch<SetStateAction<string>>;
setName: Dispatch<SetStateAction<string>>;
setRole: Dispatch<SetStateAction<ROLES>>;
id: string;
}
export default EditMembersDetails;

View File

@@ -0,0 +1,16 @@
import { Select, Typography } from 'antd';
import styled from 'styled-components';
export const SelectDrawer = styled(Select)`
width: 120px;
`;
export const Title = styled(Typography)`
width: 7rem;
`;
export const InputGroup = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;

View File

@@ -11,7 +11,7 @@ import {
} from 'antd';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { InviteMemberFormValues } from '../utils';
import { InviteMemberFormValues } from '../PendingInvitesContainer/index';
import { SelectDrawer, SpaceContainer, TitleWrapper } from './styles';
function InviteTeamMembers({ form, onFinish }: Props): JSX.Element {

View File

@@ -6,7 +6,7 @@ import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import InviteTeamMembers from '../InviteTeamMembers';
import { InviteMemberFormValues } from '../utils';
import { InviteMemberFormValues } from '../PendingInvitesContainer';
export interface InviteUserModalProps {
isInviteTeamMemberModalOpen: boolean;

View File

@@ -0,0 +1,324 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import {
Button,
Modal,
Space,
TableColumnsType as ColumnsType,
Typography,
} from 'antd';
import getAll from 'api/v1/user/get';
import deleteUser from 'api/v1/user/id/delete';
import update from 'api/v1/user/id/update';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { ResizeTable } from 'components/ResizeTable';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import DeleteMembersDetails from '../DeleteMembersDetails';
import EditMembersDetails from '../EditMembersDetails';
function UserFunction({
setDataSource,
accessLevel,
name,
email,
id,
}: UserFunctionProps): JSX.Element {
const [isModalVisible, setIsModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const onModalToggleHandler = (
func: Dispatch<SetStateAction<boolean>>,
value: boolean,
): void => {
func(value);
};
const [emailAddress, setEmailAddress] = useState(email);
const [updatedName, setUpdatedName] = useState(name);
const [role, setRole] = useState<ROLES>(accessLevel);
const { t } = useTranslation(['common']);
const [isDeleteLoading, setIsDeleteLoading] = useState<boolean>(false);
const [isUpdateLoading, setIsUpdateLoading] = useState<boolean>(false);
const { notifications } = useNotifications();
const onUpdateDetailsHandler = (): void => {
setDataSource((data) => {
const index = data.findIndex((e) => e.id === id);
if (index !== -1) {
const current = data[index];
const updatedData: DataType[] = [
...data.slice(0, index),
{
...current,
name: updatedName,
accessLevel: role,
email: emailAddress,
},
...data.slice(index + 1, data.length),
];
return updatedData;
}
return data;
});
};
const onDelete = (): void => {
setDataSource((source) => {
const index = source.findIndex((e) => e.id === id);
if (index !== -1) {
const updatedData: DataType[] = [
...source.slice(0, index),
...source.slice(index + 1, source.length),
];
return updatedData;
}
return source;
});
};
const onDeleteHandler = async (): Promise<void> => {
try {
setIsDeleteLoading(true);
await deleteUser({
userId: id,
});
onDelete();
notifications.success({
message: t('success', {
ns: 'common',
}),
});
setIsDeleteModalVisible(false);
setIsDeleteLoading(false);
} catch (error) {
setIsDeleteLoading(false);
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
};
const onEditMemberDetails = async (): Promise<void> => {
try {
setIsUpdateLoading(true);
await update({
userId: id,
displayName: updatedName,
role,
});
onUpdateDetailsHandler();
if (role !== accessLevel) {
notifications.success({
message: 'User details updated successfully',
description: 'The user details have been updated successfully.',
});
} else {
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
setIsUpdateLoading(false);
setIsModalVisible(false);
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
setIsUpdateLoading(false);
}
};
return (
<>
<Space direction="horizontal">
<Typography.Link
onClick={(): void => onModalToggleHandler(setIsModalVisible, true)}
>
Edit
</Typography.Link>
<Typography.Link
onClick={(): void => onModalToggleHandler(setIsDeleteModalVisible, true)}
>
Delete
</Typography.Link>
</Space>
<Modal
title="Edit member details"
className="edit-member-details-modal"
open={isModalVisible}
onOk={(): void => onModalToggleHandler(setIsModalVisible, false)}
onCancel={(): void => onModalToggleHandler(setIsModalVisible, false)}
centered
destroyOnClose
footer={[
<Button
key="back"
onClick={(): void => onModalToggleHandler(setIsModalVisible, false)}
type="default"
>
Cancel
</Button>,
<Button
key="Invite_team_members"
onClick={onEditMemberDetails}
type="primary"
disabled={isUpdateLoading}
loading={isUpdateLoading}
>
Update Details
</Button>,
]}
>
<EditMembersDetails
{...{
emailAddress,
name: updatedName,
role,
setEmailAddress,
setName: setUpdatedName,
setRole,
id,
}}
/>
</Modal>
<Modal
title="Edit member details"
open={isDeleteModalVisible}
onOk={onDeleteHandler}
onCancel={(): void => onModalToggleHandler(setIsDeleteModalVisible, false)}
centered
confirmLoading={isDeleteLoading}
>
<DeleteMembersDetails name={name} />
</Modal>
</>
);
}
function Members(): JSX.Element {
const { org } = useAppContext();
const { data, isLoading, error } = useQuery({
queryFn: () => getAll(),
queryKey: ['getOrgUser', org?.[0].id],
});
const [dataSource, setDataSource] = useState<DataType[]>([]);
useEffect(() => {
if (data?.data && Array.isArray(data.data)) {
const updatedData: DataType[] = data?.data?.map((e) => ({
accessLevel: e.role,
email: e.email,
id: String(e.id),
joinedOn: String(e.createdAt),
name: e.displayName,
}));
setDataSource(updatedData);
}
}, [data]);
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: 'Emails',
dataIndex: 'email',
key: 'email',
width: 100,
},
{
title: 'Access Level',
dataIndex: 'accessLevel',
key: 'accessLevel',
width: 50,
},
{
title: 'Joined On',
dataIndex: 'joinedOn',
key: 'joinedOn',
width: 60,
render: (_, record): JSX.Element => {
const { joinedOn } = record;
return (
<Typography>
{dayjs(joinedOn).format(DATE_TIME_FORMATS.MONTH_DATE_FULL)}
</Typography>
);
},
},
{
title: 'Action',
dataIndex: 'action',
width: 80,
render: (_, record): JSX.Element => (
<UserFunction
{...{
accessLevel: record.accessLevel,
email: record.email,
joinedOn: record.joinedOn,
name: record.name,
id: record.id,
setDataSource,
}}
/>
),
},
];
return (
<div className="members-container">
<Typography.Title level={3}>
Members{' '}
{!isLoading && dataSource && (
<div className="members-count"> ({dataSource.length}) </div>
)}
</Typography.Title>
{!(error as APIError) && (
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
pagination={false}
loading={isLoading}
bordered
/>
)}
{(error as APIError) && <ErrorContent error={error as APIError} />}
</div>
);
}
interface DataType {
id: string;
name: string;
email: string;
accessLevel: ROLES;
joinedOn: string;
}
interface UserFunctionProps extends DataType {
setDataSource: Dispatch<SetStateAction<DataType[]>>;
}
export default Members;

View File

@@ -0,0 +1,248 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
Form,
Space,
TableColumnsType as ColumnsType,
Typography,
} from 'antd';
import get from 'api/v1/invite/get';
import deleteInvite from 'api/v1/invite/id/delete';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { ResizeTable } from 'components/ResizeTable';
import { INVITE_MEMBERS_HASH } from 'constants/app';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { PendingInvite } from 'types/api/user/getPendingInvites';
import { ROLES } from 'types/roles';
import InviteUserModal from '../InviteUserModal/InviteUserModal';
import { TitleWrapper } from './styles';
function PendingInvitesContainer(): JSX.Element {
const [
isInviteTeamMemberModalOpen,
setIsInviteTeamMemberModalOpen,
] = useState<boolean>(false);
const [form] = Form.useForm<InviteMemberFormValues>();
const { t } = useTranslation(['organizationsettings', 'common']);
const [state, setText] = useCopyToClipboard();
const { notifications } = useNotifications();
const { user } = useAppContext();
useEffect(() => {
if (state.error) {
notifications.error({
message: state.error.message,
});
}
if (state.value) {
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
}, [state.error, state.value, t, notifications]);
const { data, isLoading, error, isError, refetch } = useQuery({
queryFn: get,
queryKey: ['getPendingInvites', user?.accessJwt],
});
const [dataSource, setDataSource] = useState<DataProps[]>([]);
const toggleModal = useCallback(
(value: boolean): void => {
setIsInviteTeamMemberModalOpen(value);
if (!value) {
form.resetFields();
}
},
[form],
);
const { hash } = useLocation();
const getParsedInviteData = useCallback(
(payload: PendingInvite[] = []) =>
payload?.map((data) => ({
key: data.createdAt,
name: data.name,
id: data.id,
email: data.email,
accessLevel: data.role,
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
})),
[],
);
useEffect(() => {
if (hash === INVITE_MEMBERS_HASH) {
toggleModal(true);
}
}, [hash, toggleModal]);
useEffect(() => {
if (data?.data) {
const parsedData = getParsedInviteData(data?.data || []);
setDataSource(parsedData);
}
}, [data, getParsedInviteData]);
const onRevokeHandler = async (id: string): Promise<void> => {
try {
await deleteInvite({
id,
});
// remove from the client data
const index = dataSource.findIndex((e) => e.id === id);
if (index !== -1) {
setDataSource([
...dataSource.slice(0, index),
...dataSource.slice(index + 1, dataSource.length),
]);
}
notifications.success({
message: t('success', {
ns: 'common',
}),
});
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
};
const columns: ColumnsType<DataProps> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: 'Emails',
dataIndex: 'email',
key: 'email',
width: 80,
},
{
title: 'Access Level',
dataIndex: 'accessLevel',
key: 'accessLevel',
width: 50,
},
{
title: 'Invite Link',
dataIndex: 'inviteLink',
key: 'Invite Link',
ellipsis: true,
width: 100,
},
{
title: 'Action',
dataIndex: 'action',
width: 80,
key: 'Action',
render: (_, record): JSX.Element => (
<Space direction="horizontal">
<Typography.Link onClick={(): Promise<void> => onRevokeHandler(record.id)}>
Revoke
</Typography.Link>
<Typography.Link
onClick={(): void => {
setText(record.inviteLink);
}}
>
Copy Invite Link
</Typography.Link>
</Space>
),
},
];
return (
<div className="pending-invites-container-wrapper">
<InviteUserModal
form={form}
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
toggleModal={toggleModal}
onClose={refetch}
/>
<div className="pending-invites-container">
<TitleWrapper>
<Typography.Title level={3}>
{t('pending_invites')}
{dataSource && (
<div className="members-count"> ({dataSource.length})</div>
)}
</Typography.Title>
<Space>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={(): void => {
toggleModal(true);
}}
>
{t('invite_members')}
</Button>
</Space>
</TitleWrapper>
{!isError && (
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
pagination={false}
loading={isLoading}
bordered
/>
)}
{isError && <ErrorContent error={error as APIError} />}
</div>
</div>
);
}
export interface InviteTeamMembersProps {
email: string;
name: string;
role: string;
id: string;
frontendBaseUrl: string;
}
interface DataProps {
key: number;
name: string;
id: string;
email: string;
accessLevel: ROLES;
inviteLink: string;
}
type Role = 'ADMIN' | 'VIEWER' | 'EDITOR';
export interface InviteMemberFormValues {
members: {
email: string;
name: string;
role: Role;
}[];
}
export default PendingInvitesContainer;

View File

@@ -0,0 +1,8 @@
import styled from 'styled-components';
export const TitleWrapper = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`;

View File

@@ -3,6 +3,8 @@ import { useAppContext } from 'providers/App/App';
import AuthDomain from './AuthDomain';
import DisplayName from './DisplayName';
import Members from './Members';
import PendingInvitesContainer from './PendingInvitesContainer';
import './OrganizationSettings.styles.scss';
@@ -21,6 +23,9 @@ function OrganizationSettings(): JSX.Element {
))}
</Space>
<PendingInvitesContainer />
<Members />
<AuthDomain />
</div>
);

View File

@@ -0,0 +1,37 @@
import { act, render, screen, waitFor } from 'tests/test-utils';
import Members from '../Members';
describe('Organization Settings Page', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('render list of members', async () => {
act(() => {
render(<Members />);
});
const title = await screen.findByText(/Members/i);
expect(title).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
});
});
// this is required as our edit/delete logic is dependent on the index and it will break with pagination enabled
it('render list of members without pagination', async () => {
render(<Members />);
await waitFor(() => {
expect(screen.getByText('firstUser@test.io')).toBeInTheDocument(); // first item
expect(screen.getByText('lastUser@test.io')).toBeInTheDocument(); // last item
expect(
document.querySelector('.ant-table-pagination'),
).not.toBeInTheDocument();
});
});
});

View File

@@ -1,17 +0,0 @@
export interface InviteTeamMembersProps {
email: string;
name: string;
role: string;
id: string;
frontendBaseUrl: string;
}
type Role = 'ADMIN' | 'VIEWER' | 'EDITOR';
export interface InviteMemberFormValues {
members: {
email: string;
name: string;
role: Role;
}[];
}

View File

@@ -100,17 +100,6 @@ interface QueryBuilderSearchV2Props {
// Determines whether to call onChange when a tag is closed
triggerOnChangeOnClose?: boolean;
skipQueryBuilderRedirect?: boolean;
/** Additional props passed through to the underlying Ant Design Select (e.g. listHeight, listItemHeight) */
selectProps?: Partial<
Pick<
React.ComponentProps<typeof Select>,
| 'listHeight'
| 'listItemHeight'
| 'popupClassName'
| 'dropdownMatchSelectWidth'
| 'popupMatchSelectWidth'
>
>;
}
export interface Option {
@@ -153,7 +142,6 @@ function QueryBuilderSearchV2(
hideSpanScopeSelector,
triggerOnChangeOnClose,
skipQueryBuilderRedirect,
selectProps,
} = props;
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@@ -984,7 +972,6 @@ function QueryBuilderSearchV2(
return (
<div className="query-builder-search-v2">
<Select
{...selectProps}
data-testid={'qb-search-select'}
ref={selectRef}
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}
@@ -1090,7 +1077,6 @@ QueryBuilderSearchV2.defaultProps = {
hideSpanScopeSelector: true,
triggerOnChangeOnClose: false,
skipQueryBuilderRedirect: false,
selectProps: undefined,
};
export default QueryBuilderSearchV2;

View File

@@ -160,7 +160,6 @@ function Filters({
onChange={handleFilterChange}
hideSpanScopeSelector={false}
skipQueryBuilderRedirect
selectProps={{ listHeight: 125 }}
/>
{filteredSpanIds.length > 0 && (
<div className="pre-next-toggle">

View File

@@ -11,17 +11,8 @@ export default {
name: 'dashboards',
type: 'metaresources',
},
{
name: 'role',
type: 'role',
},
{
name: 'roles',
type: 'metaresources',
},
],
relations: {
assignee: ['role'],
create: ['metaresources'],
delete: ['user', 'role', 'organization', 'metaresource'],
list: ['metaresources'],

View File

@@ -1,129 +0,0 @@
import { renderHook } from '@testing-library/react';
import {
IBuilderFormula,
IClickHouseQuery,
IPromQLQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { useGetQueryLabels } from './useGetQueryLabels';
jest.mock('components/QueryBuilderV2/utils', () => ({
getQueryLabelWithAggregation: jest.fn(() => []),
}));
function buildQuery(overrides: Partial<Query> = {}): Query {
return {
id: 'test-id',
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
...overrides,
};
}
describe('useGetQueryLabels', () => {
describe('QUERY_BUILDER type', () => {
it('returns empty array when queryFormulas is undefined', () => {
const query = buildQuery({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: (undefined as unknown) as IBuilderFormula[],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns formula labels when queryFormulas is populated', () => {
const query = buildQuery({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [
({ queryName: 'F1' } as unknown) as IBuilderFormula,
({ queryName: 'F2' } as unknown) as IBuilderFormula,
],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'F1', value: 'F1' },
{ label: 'F2', value: 'F2' },
]);
});
});
describe('CLICKHOUSE type', () => {
it('returns empty array when clickhouse_sql is undefined', () => {
const query = buildQuery({
queryType: EQueryType.CLICKHOUSE,
clickhouse_sql: (undefined as unknown) as IClickHouseQuery[],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns labels from clickhouse_sql when populated', () => {
const query = buildQuery({
queryType: EQueryType.CLICKHOUSE,
clickhouse_sql: [
({ name: 'query_a' } as unknown) as IClickHouseQuery,
({ name: 'query_b' } as unknown) as IClickHouseQuery,
],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'query_a', value: 'query_a' },
{ label: 'query_b', value: 'query_b' },
]);
});
});
describe('PROM type (default)', () => {
it('returns empty array when promql is undefined', () => {
const query = buildQuery({
queryType: EQueryType.PROM,
promql: (undefined as unknown) as IPromQLQuery[],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns labels from promql when populated', () => {
const query = buildQuery({
queryType: EQueryType.PROM,
promql: [
({ name: 'prom_1' } as unknown) as IPromQLQuery,
({ name: 'prom_2' } as unknown) as IPromQLQuery,
],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'prom_1', value: 'prom_1' },
{ label: 'prom_2', value: 'prom_2' },
]);
});
});
});

View File

@@ -11,7 +11,7 @@ export const useGetQueryLabels = (
const queryLabels = getQueryLabelWithAggregation(
currentQuery?.builder?.queryData || [],
);
const formulaLabels = (currentQuery?.builder?.queryFormulas ?? []).map(
const formulaLabels = currentQuery?.builder?.queryFormulas?.map(
(formula) => ({
label: formula.queryName,
value: formula.queryName,
@@ -20,13 +20,10 @@ export const useGetQueryLabels = (
return [...queryLabels, ...formulaLabels];
}
if (currentQuery?.queryType === EQueryType.CLICKHOUSE) {
return (currentQuery?.clickhouse_sql ?? []).map((q) => ({
return currentQuery?.clickhouse_sql?.map((q) => ({
label: q.name,
value: q.name,
}));
}
return (currentQuery?.promql ?? []).map((q) => ({
label: q.name,
value: q.name,
}));
return currentQuery?.promql?.map((q) => ({ label: q.name, value: q.name }));
}, [currentQuery]);

View File

@@ -1,33 +0,0 @@
import { useCallback } from 'react';
import { QueryParams } from 'constants/query';
import { parseAsString, useQueryState } from 'nuqs';
interface UseUrlYAxisUnitResult {
yAxisUnit: string;
onUnitChange: (value: string) => void;
}
/**
* Hook to manage y-axis unit synchronized with the URL query param.
* It:
* - Initializes from `QueryParams.yAxisUnit` or a provided default
* - Keeps local state in sync when the URL changes (e.g. back/forward, shared links)
* - Writes updates back to the URL while preserving existing query params
*/
function useUrlYAxisUnit(defaultUnit = ''): UseUrlYAxisUnitResult {
const [yAxisUnit, setYAxisUnitInUrl] = useQueryState(
QueryParams.yAxisUnit,
parseAsString.withDefault(defaultUnit),
);
const onUnitChange = useCallback(
(value: string): void => {
setYAxisUnitInUrl(value || null);
},
[setYAxisUnitInUrl],
);
return { yAxisUnit, onUnitChange };
}
export default useUrlYAxisUnit;

View File

@@ -6,7 +6,6 @@ import { Provider } from 'react-redux';
import AppRoutes from 'AppRoutes';
import { AxiosError } from 'axios';
import { ThemeProvider } from 'hooks/useDarkMode';
import { NuqsAdapter } from 'nuqs/adapters/react';
import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
@@ -46,19 +45,17 @@ if (container) {
root.render(
<HelmetProvider>
<NuqsAdapter>
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppProvider>
<AppRoutes />
</AppProvider>
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</NuqsAdapter>
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppProvider>
<AppRoutes />
</AppProvider>
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</HelmetProvider>,
);
}

View File

@@ -32,8 +32,8 @@ export const editSlackDescriptionDefaultValue = `{{ range .Alerts -}} *Alert:* {
export const pagerDutyDescriptionDefaultVaule = `{{ if gt (len .Alerts.Firing) 0 -}} Alerts Firing: {{ range .Alerts.Firing }} - Message: {{ .Annotations.description }} Labels: {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Annotations: {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Source: {{ .GeneratorURL }} {{ end }} {{- end }} {{ if gt (len .Alerts.Resolved) 0 -}} Alerts Resolved: {{ range .Alerts.Resolved }} - Message: {{ .Annotations.description }} Labels: {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Annotations: {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }} Source: {{ .GeneratorURL }} {{ end }} {{- end }}`;
export const pagerDutyAdditionalDetailsDefaultValue = JSON.stringify({
firing: `{{ .Alerts.Firing | toJson }}`,
resolved: `{{ .Alerts.Resolved | toJson }}`,
firing: `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
resolved: `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
num_firing: '{{ .Alerts.Firing | len }}',
num_resolved: '{{ .Alerts.Resolved | len }}',
});

View File

@@ -4,6 +4,7 @@ import {
SetStateAction,
useEffect,
useMemo,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -15,7 +16,6 @@ import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import APIError from 'types/api/error';
@@ -52,8 +52,13 @@ function TimeSeriesViewContainer({
return isValid.every(Boolean);
}, [currentQuery]);
const defaultUnit = isValidToConvertToMs ? 'ms' : 'short';
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit(defaultUnit);
const [yAxisUnit, setYAxisUnit] = useState<string>(
isValidToConvertToMs ? 'ms' : 'short',
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
@@ -116,7 +121,7 @@ function TimeSeriesViewContainer({
return (
<div className="trace-explorer-time-series-view-container">
<div className="trace-explorer-time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
</div>
<TimeSeriesView
isFilterApplied={isFilterApplied}

View File

@@ -46,10 +46,6 @@ import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import {
DASHBOARD_CACHE_TIME,
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from '../../constants/queryCacheTime';
import { useDashboardVariablesSelector } from '../../hooks/dashboard/useDashboardVariables';
import {
setDashboardVariablesStore,
@@ -68,6 +64,7 @@ export const DashboardContext = createContext<IDashboardContext>({
APIError
>,
selectedDashboard: {} as Dashboard,
dashboardId: '',
layouts: [],
panelMap: {},
setPanelMap: () => {},
@@ -80,6 +77,8 @@ export const DashboardContext = createContext<IDashboardContext>({
updateLocalStorageDashboardVariables: () => {},
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: () => {},
selectedRowWidgetId: '',
setSelectedRowWidgetId: () => {},
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: () => {},
@@ -99,6 +98,10 @@ export function DashboardProvider({
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [selectedRowWidgetId, setSelectedRowWidgetId] = useState<string | null>(
null,
);
const [
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
@@ -269,12 +272,7 @@ export function DashboardProvider({
return data;
};
const dashboardResponse = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
isDashboardPage?.params,
dashboardId,
globalTime.isAutoRefreshDisabled,
],
[REACT_QUERY_KEY.DASHBOARD_BY_ID, isDashboardPage?.params, dashboardId],
{
enabled: (!!isDashboardPage || !!isDashboardWidgetPage) && isLoggedIn,
queryFn: async () => {
@@ -291,9 +289,6 @@ export function DashboardProvider({
}
},
refetchOnWindowFocus: false,
cacheTime: globalTime.isAutoRefreshDisabled
? DASHBOARD_CACHE_TIME
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
onError: (error) => {
showErrorModal(error as APIError);
},
@@ -461,6 +456,8 @@ export function DashboardProvider({
updateLocalStorageDashboardVariables,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
setColumnWidths,
@@ -479,6 +476,8 @@ export function DashboardProvider({
currentDashboard,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
selectedRowWidgetId,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
setColumnWidths,

View File

@@ -1,10 +1,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react';
import getDashboard from 'api/v1/dashboards/id/get';
import { DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { DashboardProvider, useDashboard } from 'providers/Dashboard/Dashboard';
@@ -50,7 +47,6 @@ jest.mock('react-redux', () => ({
selectedTime: 'GLOBAL_TIME',
minTime: '2023-01-01T00:00:00Z',
maxTime: '2023-01-01T01:00:00Z',
isAutoRefreshDisabled: true,
})),
useDispatch: jest.fn(() => jest.fn()),
}));
@@ -58,9 +54,8 @@ jest.mock('react-redux', () => ({
jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
function TestComponent(): JSX.Element {
const { dashboardResponse, selectedDashboard } = useDashboard();
const { dashboardResponse, dashboardId, selectedDashboard } = useDashboard();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = selectedDashboard?.id;
return (
<div>
@@ -327,68 +322,13 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
REACT_QUERY_KEY.DASHBOARD_BY_ID,
{ dashboardId: dashboardId1 },
dashboardId1,
true, // globalTime.isAutoRefreshDisabled
]);
expect(cacheKeys[1]).toEqual([
REACT_QUERY_KEY.DASHBOARD_BY_ID,
{ dashboardId: dashboardId2 },
dashboardId2,
true, // globalTime.isAutoRefreshDisabled
]);
});
it('should not store dashboard in cache when autoRefresh is enabled (isAutoRefreshDisabled=false)', async () => {
jest.mocked(useSelector).mockImplementation(() => ({
selectedTime: 'GLOBAL_TIME',
minTime: '2023-01-01T00:00:00Z',
maxTime: '2023-01-01T01:00:00Z',
isAutoRefreshDisabled: false,
}));
const queryClient = createTestQueryClient();
const dashboardId = 'auto-refresh-dashboard';
mockUseRouteMatch.mockReturnValue({
path: ROUTES.DASHBOARD,
url: `/dashboard/${dashboardId}`,
isExact: true,
params: { dashboardId },
});
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId}`]}>
<DashboardProvider>
<TestComponent />
</DashboardProvider>
</MemoryRouter>
</QueryClientProvider>,
);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId });
});
const dashboardQuery = queryClient
.getQueryCache()
.getAll()
.find(
(query) =>
query.queryKey[0] === REACT_QUERY_KEY.DASHBOARD_BY_ID &&
query.queryKey[3] === false,
);
expect(dashboardQuery).toBeDefined();
expect((dashboardQuery as { cacheTime: number }).cacheTime).toBe(
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
);
jest.mocked(useSelector).mockImplementation(() => ({
selectedTime: 'GLOBAL_TIME',
minTime: '2023-01-01T00:00:00Z',
maxTime: '2023-01-01T01:00:00Z',
isAutoRefreshDisabled: true,
}));
});
});
});

View File

@@ -1,28 +0,0 @@
const PREFIX = 'dashboard_row_widget_';
function getKey(dashboardId: string): string {
return `${PREFIX}${dashboardId}`;
}
export function setSelectedRowWidgetId(
dashboardId: string,
widgetId: string,
): void {
const key = getKey(dashboardId);
// remove all other selected widget ids for the dashboard before setting the new one
// to ensure only one widget is selected at a time. Helps out in weird navigate and refresh scenarios
Object.keys(sessionStorage)
.filter((k) => k.startsWith(PREFIX) && k !== key)
.forEach((k) => sessionStorage.removeItem(k));
sessionStorage.setItem(key, widgetId);
}
export function getSelectedRowWidgetId(dashboardId: string): string | null {
return sessionStorage.getItem(getKey(dashboardId));
}
export function clearSelectedRowWidgetId(dashboardId: string): void {
sessionStorage.removeItem(getKey(dashboardId));
}

View File

@@ -15,6 +15,7 @@ export interface IDashboardContext {
handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined;
dashboardId: string;
layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
@@ -39,6 +40,8 @@ export interface IDashboardContext {
) => void;
dashboardQueryRangeCalled: boolean;
setDashboardQueryRangeCalled: (value: boolean) => void;
selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
isDashboardFetching: boolean;
columnWidths: WidgetColumnWidths;
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;

View File

@@ -7,7 +7,6 @@ import { render, RenderOptions, RenderResult } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { NuqsAdapter } from 'nuqs/adapters/react';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
@@ -281,25 +280,23 @@ export function AllTheProviders({
return (
<MemoryRouter initialEntries={[initialRouteValue]}>
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
<Provider store={mockStored(roleValue)}>
<AppContext.Provider
value={getAppContextMock(roleValue, appContextOverridesValue)}
>
<ResourceProvider>
<ErrorModalProvider>
<TimezoneProvider>
<PreferenceContextProvider>
{queryBuilderContent}
</PreferenceContextProvider>
</TimezoneProvider>
</ErrorModalProvider>
</ResourceProvider>
</AppContext.Provider>
</Provider>
</QueryClientProvider>
</NuqsAdapter>
<QueryClientProvider client={queryClient}>
<Provider store={mockStored(roleValue)}>
<AppContext.Provider
value={getAppContextMock(roleValue, appContextOverridesValue)}
>
<ResourceProvider>
<ErrorModalProvider>
<TimezoneProvider>
<PreferenceContextProvider>
{queryBuilderContent}
</PreferenceContextProvider>
</TimezoneProvider>
</ErrorModalProvider>
</ResourceProvider>
</AppContext.Provider>
</Provider>
</QueryClientProvider>
</MemoryRouter>
);
}

354
go.mod
View File

@@ -1,51 +1,51 @@
module github.com/SigNoz/signoz
go 1.25.0
go 1.24.0
require (
dario.cat/mergo v1.0.2
dario.cat/mergo v1.0.1
github.com/AfterShip/clickhouse-sql-parser v0.4.16
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.144.2
github.com/SigNoz/signoz-otel-collector v0.129.10-rc.9
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3
github.com/bytedance/sonic v1.14.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/coreos/go-oidc/v3 v3.14.1
github.com/dgraph-io/ristretto/v2 v2.3.0
github.com/dustin/go-humanize v1.0.1
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron v1.30.1
github.com/go-openapi/runtime v0.29.2
github.com/go-openapi/strfmt v0.25.0
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/go-redis/redismock/v9 v9.2.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/gojek/heimdall/v7 v7.0.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/huandu/go-sqlbuilder v1.35.0
github.com/jackc/pgx/v5 v5.7.6
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
github.com/json-iterator/go v1.1.12
github.com/knadh/koanf v1.5.0
github.com/knadh/koanf/v2 v2.3.2
github.com/mailru/easyjson v0.9.0
github.com/open-telemetry/opamp-go v0.22.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0
github.com/knadh/koanf/v2 v2.2.0
github.com/mailru/easyjson v0.7.7
github.com/open-telemetry/opamp-go v0.19.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.128.0
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe
github.com/opentracing/opentracing-go v1.2.0
github.com/pkg/errors v0.9.1
github.com/prometheus/alertmanager v0.31.0
github.com/prometheus/alertmanager v0.28.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5
github.com/prometheus/prometheus v0.310.0
github.com/prometheus/common v0.66.1
github.com/prometheus/prometheus v0.304.1
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1
github.com/redis/go-redis/v9 v9.17.2
github.com/redis/go-redis/v9 v9.15.1
github.com/rs/cors v1.11.1
github.com/russellhaering/gosaml2 v0.9.0
github.com/russellhaering/goxmldsig v1.2.0
@@ -54,7 +54,7 @@ require (
github.com/sethvargo/go-password v0.2.0
github.com/smartystreets/goconvey v1.8.1
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.10.2
github.com/spf13/cobra v1.10.1
github.com/srikanthccv/ClickHouse-go-mock v0.13.0
github.com/stretchr/testify v1.11.1
github.com/swaggest/jsonschema-go v0.3.78
@@ -64,71 +64,43 @@ require (
github.com/uptrace/bun/dialect/pgdialect v1.2.9
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
github.com/uptrace/bun/extra/bunotel v1.2.9
go.opentelemetry.io/collector/confmap v1.51.0
go.opentelemetry.io/collector/otelcol v0.144.0
go.opentelemetry.io/collector/pdata v1.51.0
go.opentelemetry.io/collector/confmap v1.34.0
go.opentelemetry.io/collector/otelcol v0.128.0
go.opentelemetry.io/collector/pdata v1.34.0
go.opentelemetry.io/contrib/config v0.10.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.33.0
google.golang.org/protobuf v1.36.11
golang.org/x/text v0.32.0
google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.35.0
k8s.io/apimachinery v0.34.0
modernc.org/sqlite v1.39.1
)
require (
github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/swaggest/refl v1.4.0 // indirect
@@ -136,70 +108,69 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/collector/client v1.50.0 // indirect
go.opentelemetry.io/collector/config/configoptional v1.50.0 // indirect
go.opentelemetry.io/collector/config/configretry v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exporterhelper v0.144.0 // indirect
go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/xpdata v0.144.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
cloud.google.com/go/compute/metadata v0.8.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/ClickHouse/ch-go v0.67.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/Yiling-J/theine-go v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/coder/quartz v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/coder/quartz v0.1.2 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/elastic/lunes v0.2.0 // indirect
github.com/elastic/lunes v0.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/expr-lang/expr v1.17.7
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/expr-lang/expr v1.17.5
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.24.2 // indirect
github.com/go-openapi/errors v0.22.6 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/loads v0.23.2 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
github.com/golang/protobuf v1.5.4 // indirect
@@ -207,22 +178,22 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/memberlist v0.5.4 // indirect
github.com/hashicorp/memberlist v0.5.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -230,25 +201,26 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-syslog/v4 v4.3.0 // indirect
github.com/leodido/go-syslog/v4 v4.2.0 // indirect
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/miekg/dns v1.1.65 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -257,27 +229,27 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/natefinch/wrap v0.2.0 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.1
github.com/open-feature/go-sdk v1.17.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.144.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.128.0 // indirect
github.com/openfga/openfga v1.10.1
github.com/paulmach/orb v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pressly/goose/v3 v3.25.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/exporter-toolkit v0.15.1 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/sigv4 v0.4.1 // indirect
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
github.com/prometheus/otlptranslator v0.0.0-20250320144820-d800c8b0eb07 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/sigv4 v0.1.2 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
@@ -285,7 +257,7 @@ require (
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
github.com/shirou/gopsutil/v4 v4.25.5 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
@@ -300,92 +272,94 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggest/openapi-go v0.2.60
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vjeantet/grok v1.0.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/component v1.51.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.145.0 // indirect
go.opentelemetry.io/collector/component/componenttest v0.145.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.144.0 // indirect
go.opentelemetry.io/collector/confmap/provider/envprovider v1.50.0 // indirect
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.50.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 // indirect
go.opentelemetry.io/collector/connector v0.144.0 // indirect
go.opentelemetry.io/collector/connector/connectortest v0.144.0 // indirect
go.opentelemetry.io/collector/connector/xconnector v0.144.0 // indirect
go.opentelemetry.io/collector/consumer v1.51.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.144.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.145.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 // indirect
go.opentelemetry.io/collector/exporter v1.50.0 // indirect
go.opentelemetry.io/collector/exporter/exportertest v0.144.0 // indirect
go.opentelemetry.io/collector/exporter/xexporter v0.144.0 // indirect
go.opentelemetry.io/collector/extension v1.50.0 // indirect
go.opentelemetry.io/collector/extension/extensioncapabilities v0.144.0 // indirect
go.opentelemetry.io/collector/extension/extensiontest v0.144.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.144.0 // indirect
go.opentelemetry.io/collector/featuregate v1.51.0 // indirect
go.opentelemetry.io/collector/internal/fanoutconsumer v0.144.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.144.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.145.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.145.0 // indirect
go.opentelemetry.io/collector/pipeline v1.51.0 // indirect
go.opentelemetry.io/collector/pipeline/xpipeline v0.144.0 // indirect
go.opentelemetry.io/collector/processor v1.51.0 // indirect
go.opentelemetry.io/collector/processor/processorhelper v0.144.0 // indirect
go.opentelemetry.io/collector/processor/processortest v0.145.0 // indirect
go.opentelemetry.io/collector/processor/xprocessor v0.145.0 // indirect
go.opentelemetry.io/collector/receiver v1.50.0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.144.0 // indirect
go.opentelemetry.io/collector/receiver/receivertest v0.144.0 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.144.0 // indirect
go.opentelemetry.io/collector/semconv v0.128.1-0.20250610090210-188191247685
go.opentelemetry.io/collector/service v0.144.0 // indirect
go.opentelemetry.io/collector/service/hostcapabilities v0.144.0 // indirect
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 // indirect
go.opentelemetry.io/contrib/otelconf v0.18.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.mongodb.org/mongo-driver v1.17.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/component v1.34.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.128.0 // indirect
go.opentelemetry.io/collector/component/componenttest v0.128.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.128.0 // indirect
go.opentelemetry.io/collector/confmap/provider/envprovider v1.34.0 // indirect
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.34.0 // indirect
go.opentelemetry.io/collector/confmap/xconfmap v0.128.0 // indirect
go.opentelemetry.io/collector/connector v0.128.0 // indirect
go.opentelemetry.io/collector/connector/connectortest v0.128.0 // indirect
go.opentelemetry.io/collector/connector/xconnector v0.128.0 // indirect
go.opentelemetry.io/collector/consumer v1.34.0 // indirect
go.opentelemetry.io/collector/consumer/consumererror v0.128.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.128.0 // indirect
go.opentelemetry.io/collector/consumer/xconsumer v0.128.0 // indirect
go.opentelemetry.io/collector/exporter v0.128.0 // indirect
go.opentelemetry.io/collector/exporter/exportertest v0.128.0 // indirect
go.opentelemetry.io/collector/exporter/xexporter v0.128.0 // indirect
go.opentelemetry.io/collector/extension v1.34.0 // indirect
go.opentelemetry.io/collector/extension/extensioncapabilities v0.128.0 // indirect
go.opentelemetry.io/collector/extension/extensiontest v0.128.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.128.0 // indirect
go.opentelemetry.io/collector/featuregate v1.34.0 // indirect
go.opentelemetry.io/collector/internal/fanoutconsumer v0.128.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.128.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.128.0 // indirect
go.opentelemetry.io/collector/pipeline v0.128.0 // indirect
go.opentelemetry.io/collector/pipeline/xpipeline v0.128.0 // indirect
go.opentelemetry.io/collector/processor v1.34.0 // indirect
go.opentelemetry.io/collector/processor/processorhelper v0.128.0 // indirect
go.opentelemetry.io/collector/processor/processortest v0.128.0 // indirect
go.opentelemetry.io/collector/processor/xprocessor v0.128.0 // indirect
go.opentelemetry.io/collector/receiver v1.34.0 // indirect
go.opentelemetry.io/collector/receiver/receiverhelper v0.128.0 // indirect
go.opentelemetry.io/collector/receiver/receivertest v0.128.0 // indirect
go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 // indirect
go.opentelemetry.io/collector/semconv v0.128.0
go.opentelemetry.io/collector/service v0.128.0 // indirect
go.opentelemetry.io/collector/service/hostcapabilities v0.128.0 // indirect
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/contrib/otelconf v0.16.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.58.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
go.opentelemetry.io/otel/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.265.0
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.39.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.236.0
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.1 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.0 // indirect
k8s.io/client-go v0.34.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
replace github.com/expr-lang/expr => github.com/SigNoz/expr v1.17.7-beta

918
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ func (d *Dispatcher) Run() {
d.ctx, d.cancel = context.WithCancel(context.Background())
d.mtx.Unlock()
d.run(d.alerts.Subscribe(fmt.Sprintf("dispatcher-%s", d.orgID)))
d.run(d.alerts.Subscribe())
close(d.done)
}
@@ -107,15 +107,14 @@ func (d *Dispatcher) run(it provider.AlertIterator) {
for {
select {
case alertWrapper, ok := <-it.Next():
if !ok || alertWrapper == nil {
case alert, ok := <-it.Next():
if !ok {
// Iterator exhausted for some reason.
if err := it.Err(); err != nil {
d.logger.ErrorContext(d.ctx, "Error on alert update", "err", err)
}
return
}
alert := alertWrapper.Data
d.logger.DebugContext(d.ctx, "SigNoz Custom Dispatcher: Received alert", "alert", alert)

View File

@@ -365,7 +365,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -496,7 +496,7 @@ route:
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
err = alerts.Put(inputAlerts...)
if err != nil {
t.Fatal(err)
}
@@ -638,7 +638,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -798,7 +798,7 @@ route:
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
err = alerts.Put(inputAlerts...)
if err != nil {
t.Fatal(err)
}
@@ -897,7 +897,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -1028,7 +1028,7 @@ route:
err := nfManager.SetNotificationConfig(orgId, ruleID, &config)
require.NoError(t, err)
}
err = alerts.Put(ctx, inputAlerts...)
err = alerts.Put(inputAlerts...)
if err != nil {
t.Fatal(err)
}
@@ -1159,7 +1159,7 @@ func newAlert(labels model.LabelSet) *alertmanagertypes.Alert {
func TestDispatcherRace(t *testing.T) {
logger := promslog.NewNopLogger()
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -1175,7 +1175,6 @@ func TestDispatcherRace(t *testing.T) {
}
func TestDispatcherRaceOnFirstAlertNotDeliveredWhenGroupWaitIsZero(t *testing.T) {
ctx := context.Background()
const numAlerts = 5000
confData := `receivers:
- name: 'slack'
@@ -1195,7 +1194,7 @@ route:
providerSettings := createTestProviderSettings()
logger := providerSettings.Logger
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}
@@ -1248,7 +1247,7 @@ route:
for i := 0; i < numAlerts; i++ {
ruleId := fmt.Sprintf("Alert_%d", i)
alert := newAlert(model.LabelSet{"ruleId": model.LabelValue(ruleId)})
require.NoError(t, alerts.Put(ctx, alert))
require.NoError(t, alerts.Put(alert))
}
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); {
@@ -1266,7 +1265,7 @@ func TestDispatcher_DoMaintenance(t *testing.T) {
r := prometheus.NewRegistry()
marker := alertmanagertypes.NewMarker(r)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, 0, nil, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, nil, promslog.NewNopLogger(), nil)
if err != nil {
t.Fatal(err)
}
@@ -1371,7 +1370,7 @@ route:
logger := providerSettings.Logger
route := dispatch.NewRoute(conf.Route, nil)
marker := alertmanagertypes.NewMarker(prometheus.NewRegistry())
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, prometheus.NewRegistry(), nil)
alerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, nil, logger, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -190,7 +190,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
})
}()
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, 0, nil, server.logger, signozRegisterer, nil)
server.alerts, err = mem.NewAlerts(ctx, server.marker, server.srvConfig.Alerts.GCInterval, nil, server.logger, signozRegisterer)
if err != nil {
return nil, err
}
@@ -203,15 +203,15 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
func (server *Server) GetAlerts(ctx context.Context, params alertmanagertypes.GettableAlertsParams) (alertmanagertypes.GettableAlerts, error) {
return alertmanagertypes.NewGettableAlertsFromAlertProvider(server.alerts, server.alertmanagerConfig, server.marker.Status, func(labels model.LabelSet) {
server.inhibitor.Mutes(ctx, labels)
server.silencer.Mutes(ctx, labels)
server.inhibitor.Mutes(labels)
server.silencer.Mutes(labels)
}, params)
}
func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanagertypes.PostableAlerts) error {
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(ctx, postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(postableAlerts, time.Duration(server.srvConfig.Global.ResolveTimeout), time.Now())
// Notification sending alert takes precedence over validation errors.
if err := server.alerts.Put(ctx, alerts...); err != nil {
if err := server.alerts.Put(alerts...); err != nil {
return err
}
@@ -340,7 +340,6 @@ func (server *Server) TestAlert(ctx context.Context, receiversMap map[*alertmana
}
alerts, err := alertmanagertypes.NewAlertsFromPostableAlerts(
ctx,
postableAlerts,
time.Duration(server.srvConfig.Global.ResolveTimeout),
time.Now(),

View File

@@ -70,7 +70,7 @@ func TestServerTestReceiverTypeWebhook(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
URL: &config.SecretURL{URL: webhookURL},
},
},
})
@@ -96,7 +96,7 @@ func TestServerPutAlerts(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost/test-receiver"),
URL: &config.SecretURL{URL: &url.URL{Host: "localhost", Path: "/test-receiver"}},
},
},
}))
@@ -176,7 +176,7 @@ func TestServerTestAlert(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook1URL.String()),
URL: &config.SecretURL{URL: webhook1URL},
},
},
}))
@@ -186,7 +186,7 @@ func TestServerTestAlert(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhook2URL.String()),
URL: &config.SecretURL{URL: webhook2URL},
},
},
}))
@@ -268,7 +268,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL(webhookURL.String()),
URL: &config.SecretURL{URL: webhookURL},
},
},
}))
@@ -278,7 +278,7 @@ func TestServerTestAlertContinuesOnFailure(t *testing.T) {
WebhookConfigs: []*config.WebhookConfig{
{
HTTPConfig: &commoncfg.HTTPClientConfig{},
URL: config.SecretTemplateURL("http://localhost:1/webhook"),
URL: &config.SecretURL{URL: &url.URL{Scheme: "http", Host: "localhost:1", Path: "/webhook"}},
},
},
}))

View File

@@ -32,7 +32,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
Tags: []string{"users"},
Summary: "Create bulk invite",
Description: "This endpoint creates a bulk invite for a user",
Request: new(types.PostableBulkInviteRequest),
Request: make([]*types.PostableInvite, 0),
RequestContentType: "application/json",
Response: nil,
SuccessStatusCode: http.StatusCreated,

View File

@@ -17,7 +17,7 @@ func NewStore(sqlstore sqlstore.SQLStore) authtypes.AuthNStore {
return &store{sqlstore: sqlstore}
}
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) {
func (store *store) GetUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) {
user := new(types.User)
factorPassword := new(types.FactorPassword)
@@ -28,7 +28,6 @@ func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Co
Model(user).
Where("email = ?", email).
Where("org_id = ?", orgID).
Where("status = ?", types.UserStatusActive.StringValue()).
Scan(ctx)
if err != nil {
return nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with email %s in org %s not found", email, orgID)

View File

@@ -21,7 +21,7 @@ func New(store authtypes.AuthNStore) *AuthN {
}
func (a *AuthN) Authenticate(ctx context.Context, email string, password string, orgID valuer.UUID) (*authtypes.Identity, error) {
user, factorPassword, err := a.store.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID)
user, factorPassword, err := a.store.GetUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID)
if err != nil {
return nil, err
}

View File

@@ -15,7 +15,7 @@ import (
sdkmetric "go.opentelemetry.io/otel/metric"
sdkmetricnoop "go.opentelemetry.io/otel/metric/noop"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
sdktrace "go.opentelemetry.io/otel/trace"
)

View File

@@ -796,17 +796,17 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
}
opts := querybuilder.FilterExprVisitorOpts{
Context: ctx,
Logger: m.logger,
FieldMapper: m.fieldMapper,
ConditionBuilder: m.condBuilder,
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "metric_name", FieldContext: telemetrytypes.FieldContextMetric},
FieldKeys: keys,
StartNs: querybuilder.ToNanoSecs(uint64(startMillis)),
EndNs: querybuilder.ToNanoSecs(uint64(endMillis)),
}
startNs := querybuilder.ToNanoSecs(uint64(startMillis))
endNs := querybuilder.ToNanoSecs(uint64(endMillis))
whereClause, err := querybuilder.PrepareWhereClause(expression, opts, startNs, endNs)
whereClause, err := querybuilder.PrepareWhereClause(expression, opts)
if err != nil {
return nil, err
}

View File

@@ -65,9 +65,6 @@ func (module *module) GetSessionContext(ctx context.Context, email valuer.Email,
return nil, err
}
// filter out deleted users
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.ErrIfDeleted() != nil })
// Since email is a valuer, we can be sure that it is a valid email and we can split it to get the domain name.
name := strings.Split(email.String(), "@")[1]
@@ -144,7 +141,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
roleMapping := authDomain.AuthDomainConfig().RoleMapping
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID, types.UserStatusActive)
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID)
if err != nil {
return "", err
}

View File

@@ -86,15 +86,6 @@ func (module *getter) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int6
return count, nil
}
func (module *getter) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error) {
counts, err := module.store.CountByOrgIDAndStatuses(ctx, orgID, statuses)
if err != nil {
return nil, err
}
return counts, nil
}
func (module *getter) GetFactorPasswordByUserID(ctx context.Context, userID valuer.UUID) (*types.FactorPassword, error) {
factorPassword, err := module.store.GetPasswordByUserID(ctx, userID)
if err != nil {

View File

@@ -149,11 +149,16 @@ func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
render.Error(w, err)
uuid, err := valuer.NewUUID(id)
if err != nil {
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
if err := h.module.DeleteInvite(ctx, claims.OrgID, uuid); err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
@@ -213,9 +218,6 @@ func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
return
}
// temp code - show only active users
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusActive })
render.Success(w, http.StatusOK, users)
}

View File

@@ -2,6 +2,7 @@ package impluser
import (
"context"
"fmt"
"slices"
"strings"
"time"
@@ -51,50 +52,39 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
}
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
// get the user by reset password token
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
invite, err := m.store.GetInviteByToken(ctx, token)
if err != nil {
return nil, err
}
// update the password and delete the token
err = m.UpdatePasswordByResetPasswordToken(ctx, token, password)
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
if err != nil {
return nil, err
}
// query the user again
user, err = m.store.GetByOrgIDAndID(ctx, user.OrgID, user.ID)
factorPassword, err := types.NewFactorPassword(password, user.ID.StringValue())
if err != nil {
return nil, err
}
err = m.CreateUser(ctx, user, root.WithFactorPassword(factorPassword))
if err != nil {
return nil, err
}
if err := m.DeleteInvite(ctx, invite.OrgID.String(), invite.ID); err != nil {
return nil, err
}
return user, nil
}
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
// get the user
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
invite, err := m.store.GetInviteByToken(ctx, token)
if err != nil {
return nil, err
}
// create a dummy invite obj for backward compatibility
invite := &types.Invite{
Identifiable: types.Identifiable{
ID: user.ID,
},
Name: user.DisplayName,
Email: user.Email,
Token: token,
Role: user.Role,
OrgID: user.OrgID,
TimeAuditable: types.TimeAuditable{
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
},
}
return invite, nil
}
@@ -105,158 +95,80 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
return nil, err
}
// validate all emails to be invited
emails := make([]string, len(bulkInvites.Invites))
for idx, invite := range bulkInvites.Invites {
emails[idx] = invite.Email.StringValue()
invites := make([]*types.Invite, 0, len(bulkInvites.Invites))
for _, invite := range bulkInvites.Invites {
// check if user exists
existingUser, err := m.store.GetUserByEmailAndOrgID(ctx, invite.Email, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if existingUser != nil {
if err := existingUser.ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot send invite to root user")
}
}
if existingUser != nil {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with the same email")
}
// Check if an invite already exists
existingInvite, err := m.store.GetInviteByEmailAndOrgID(ctx, invite.Email, orgID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
if existingInvite != nil {
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email")
}
role, err := types.NewRole(invite.Role.String())
if err != nil {
return nil, err
}
newInvite, err := types.NewInvite(invite.Name, role, orgID, invite.Email)
if err != nil {
return nil, err
}
newInvite.InviteLink = fmt.Sprintf("%s/signup?token=%s", invite.FrontendBaseUrl, newInvite.Token)
invites = append(invites, newInvite)
}
users, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
err = m.store.CreateBulkInvite(ctx, invites)
if err != nil {
return nil, err
}
if len(users) > 0 {
if err := users[0].ErrIfRoot(); err != nil {
return nil, errors.WithAdditionalf(err, "Cannot send invite to root user")
}
for i := 0; i < len(invites); i++ {
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{"invitee_email": invites[i].Email, "invitee_role": invites[i].Role})
if users[0].Status == types.UserStatusPendingInvite {
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email: %s", users[0].Email.StringValue())
}
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with this email: %s", users[0].Email.StringValue())
}
type userWithResetToken struct {
User *types.User
ResetPasswordToken *types.ResetPasswordToken
}
newUsersWithResetToken := make([]*userWithResetToken, len(bulkInvites.Invites))
if err := m.store.RunInTx(ctx, func(ctx context.Context) error {
for idx, invite := range bulkInvites.Invites {
role, err := types.NewRole(invite.Role.String())
if err != nil {
return err
}
// create a new user with pending invite status
newUser, err := types.NewUser(invite.Name, invite.Email, role, orgID, types.UserStatusPendingInvite)
if err != nil {
return err
}
// store the user and password in db
err = m.createUserWithoutGrant(ctx, newUser)
if err != nil {
return err
}
// generate reset password token
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, newUser.ID)
if err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", "error", err)
return err
}
newUsersWithResetToken[idx] = &userWithResetToken{
User: newUser,
ResetPasswordToken: resetPasswordToken,
}
}
return nil
}); err != nil {
return nil, err
}
invites := make([]*types.Invite, len(bulkInvites.Invites))
// send password reset emails to all the invited users
for idx, userWithToken := range newUsersWithResetToken {
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{
"invitee_email": userWithToken.User.Email,
"invitee_role": userWithToken.User.Role,
})
invite := &types.Invite{
Identifiable: types.Identifiable{
ID: userWithToken.User.ID,
},
Name: userWithToken.User.DisplayName,
Email: userWithToken.User.Email,
Token: userWithToken.ResetPasswordToken.Token,
Role: userWithToken.User.Role,
OrgID: userWithToken.User.OrgID,
TimeAuditable: types.TimeAuditable{
CreatedAt: userWithToken.User.CreatedAt,
UpdatedAt: userWithToken.User.UpdatedAt,
},
}
invites[idx] = invite
frontendBaseUrl := bulkInvites.Invites[idx].FrontendBaseUrl
if frontendBaseUrl == "" {
m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", userWithToken.User.Email)
// if the frontend base url is not provided, we don't send the email
if bulkInvites.Invites[i].FrontendBaseUrl == "" {
m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", invites[i].Email)
continue
}
resetLink := userWithToken.ResetPasswordToken.FactorPasswordResetLink(frontendBaseUrl)
tokenLifetime := m.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := m.emailing.SendHTML(ctx, userWithToken.User.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
if err := m.emailing.SendHTML(ctx, invites[i].Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{
"inviter_email": creator.Email,
"link": resetLink,
"Expiry": humanizedTokenLifetime,
"link": fmt.Sprintf("%s/signup?token=%s", bulkInvites.Invites[i].FrontendBaseUrl, invites[i].Token),
}); err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to send invite email", "error", err)
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}
}
return invites, nil
}
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
// find all the users with pending_invite status
users, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
if err != nil {
return nil, err
}
return m.store.ListInvite(ctx, orgID)
}
pendingUsers := slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusPendingInvite })
var invites []*types.Invite
for _, pUser := range pendingUsers {
// get the reset password token
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, pUser.ID)
if err != nil {
return nil, err
}
// create a dummy invite obj for backward compatibility
invite := &types.Invite{
Identifiable: types.Identifiable{
ID: pUser.ID,
},
Name: pUser.DisplayName,
Email: pUser.Email,
Token: resetPasswordToken.Token,
Role: pUser.Role,
OrgID: pUser.OrgID,
TimeAuditable: types.TimeAuditable{
CreatedAt: pUser.CreatedAt,
UpdatedAt: pUser.UpdatedAt, // dummy
},
}
invites = append(invites, invite)
}
return invites, nil
func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
return m.store.DeleteInvite(ctx, orgID, id)
}
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
@@ -301,14 +213,6 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
return nil, errors.WithAdditionalf(err, "cannot update root user")
}
if err := existingUser.ErrIfDeleted(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update deleted user")
}
if err := existingUser.ErrIfPending(); err != nil {
return nil, errors.WithAdditionalf(err, "cannot update pending user")
}
requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy))
if err != nil {
return nil, err
@@ -320,7 +224,7 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
// Make sure that the request is not demoting the last admin user.
if user.Role != "" && user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin {
adminUsers, err := m.store.GetActiveUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
adminUsers, err := m.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
if err != nil {
return nil, err
}
@@ -376,16 +280,12 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return errors.WithAdditionalf(err, "cannot delete root user")
}
if err := user.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "cannot delete already deleted user")
}
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(user.Email.String())) {
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
}
// don't allow to delete the last admin user
adminUsers, err := module.store.GetActiveUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
adminUsers, err := module.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
if err != nil {
return err
}
@@ -400,8 +300,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
return err
}
// for now we are only soft deleting users
if err := module.store.SoftDeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil {
return err
}
@@ -422,10 +321,6 @@ func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID
return nil, errors.WithAdditionalf(err, "cannot reset password for root user")
}
if err := user.ErrIfDeleted(); err != nil {
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "user has been deleted")
}
password, err := module.store.GetPasswordByUserID(ctx, userID)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
@@ -480,7 +375,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "Users are not allowed to reset their password themselves, please contact an admin to reset your password.")
}
user, err := module.GetNonDeletedUserByEmailAndOrgID(ctx, email, orgID)
user, err := module.store.GetUserByEmailAndOrgID(ctx, email, orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil // for security reasons
@@ -498,7 +393,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
return err
}
resetLink := token.FactorPasswordResetLink(frontendBaseURL)
resetLink := fmt.Sprintf("%s/password-reset?token=%s", frontendBaseURL, token.Token)
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
@@ -540,11 +435,6 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
return err
}
// handle deleted user
if err := user.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "deleted users cannot reset their password")
}
if err := user.ErrIfRoot(); err != nil {
return errors.WithAdditionalf(err, "cannot reset password for root user")
}
@@ -553,38 +443,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
return err
}
// since grant is idempotent, multiple calls won't cause issues in case of retries
if user.Status == types.UserStatusPendingInvite {
if err = module.authz.Grant(
ctx,
user.OrgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
); err != nil {
return err
}
}
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if user.Status == types.UserStatusPendingInvite {
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
return err
}
if err := module.store.UpdateUser(ctx, user.OrgID, user); err != nil {
return err
}
}
if err := module.store.UpdatePassword(ctx, password); err != nil {
return err
}
if err := module.store.DeleteResetPasswordTokenByPasswordID(ctx, password.ID); err != nil {
return err
}
return nil
})
return module.store.UpdatePassword(ctx, password)
}
func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error {
@@ -593,10 +452,6 @@ func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, ol
return err
}
if err := user.ErrIfDeleted(); err != nil {
return errors.WithAdditionalf(err, "cannot change password for deleted user")
}
if err := user.ErrIfRoot(); err != nil {
return errors.WithAdditionalf(err, "cannot change password for root user")
}
@@ -614,17 +469,7 @@ func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, ol
return err
}
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.UpdatePassword(ctx, password); err != nil {
return err
}
if err := module.store.DeleteResetPasswordTokenByPasswordID(ctx, password.ID); err != nil {
return err
}
return nil
}); err != nil {
if err := module.store.UpdatePassword(ctx, password); err != nil {
return err
}
@@ -632,7 +477,7 @@ func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, ol
}
func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opts ...root.CreateUserOption) (*types.User, error) {
existingUser, err := module.GetNonDeletedUserByEmailAndOrgID(ctx, user.Email, user.OrgID)
existingUser, err := module.store.GetUserByEmailAndOrgID(ctx, user.Email, user.OrgID)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
@@ -640,16 +485,6 @@ func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opt
}
if existingUser != nil {
// for users logging through SSO flow but are having status as pending_invite
if existingUser.Status == types.UserStatusPendingInvite {
// respect the role coming from the SSO
existingUser.Update("", user.Role)
// activate the user
if err = module.activatePendingUser(ctx, existingUser); err != nil {
return nil, err
}
}
return existingUser, nil
}
@@ -726,15 +561,12 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O
func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
stats := make(map[string]any)
counts, err := module.store.CountByOrgIDAndStatuses(ctx, orgID, []string{types.UserStatusActive.StringValue(), types.UserStatusDeleted.StringValue(), types.UserStatusPendingInvite.StringValue()})
count, err := module.store.CountByOrgID(ctx, orgID)
if err == nil {
stats["user.count"] = counts[types.UserStatusActive] + counts[types.UserStatusDeleted] + counts[types.UserStatusPendingInvite]
stats["user.count.active"] = counts[types.UserStatusActive]
stats["user.count.deleted"] = counts[types.UserStatusDeleted]
stats["user.count.pending_invite"] = counts[types.UserStatusPendingInvite]
stats["user.count"] = count
}
count, err := module.store.CountAPIKeyByOrgID(ctx, orgID)
count, err = module.store.CountAPIKeyByOrgID(ctx, orgID)
if err == nil {
stats["factor.api_key.count"] = count
}
@@ -742,28 +574,6 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
// this function restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error
func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) {
existingUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
if err != nil {
return nil, err
}
// filter out the deleted users
existingUsers = slices.DeleteFunc(existingUsers, func(user *types.User) bool { return user.ErrIfDeleted() != nil })
if len(existingUsers) > 1 {
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "Multiple non-deleted users found for email %s in org_id: %s", email.StringValue(), orgID.StringValue())
}
if len(existingUsers) == 1 {
return existingUsers[0], nil
}
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "No non-deleted user found with email %s in org_id: %s", email.StringValue(), orgID.StringValue())
}
func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
createUserOpts := root.NewCreateUserOptions(opts...)
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
@@ -788,25 +598,3 @@ func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.U
return nil
}
func (module *Module) activatePendingUser(ctx context.Context, user *types.User) error {
err := module.authz.Grant(
ctx,
user.OrgID,
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {
return err
}
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
return err
}
err = module.store.UpdateUser(ctx, user.OrgID, user)
if err != nil {
return err
}
return nil
}

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