Compare commits

..

4 Commits

Author SHA1 Message Date
Yunus M
fe080b6376 feat: enhance navigation tests for middle mouse button interactions 2026-03-09 20:00:34 +05:30
Yunus M
4926481d7f feat: address review comments 2026-03-09 19:54:49 +05:30
Yunus M
32d7216537 chore: update test cases 2026-03-09 19:36:51 +05:30
Yunus M
4ece479c57 chore: support cmd click on all clickable items 2026-03-09 19:36:15 +05:30
139 changed files with 2187 additions and 3451 deletions

View File

@@ -1,22 +1,4 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.5.6
container_name: init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.5.6
container_name: clickhouse
@@ -25,7 +7,6 @@ services:
- ${PWD}/fs/etc/clickhouse-server/users.d/users.xml:/etc/clickhouse-server/users.d/users.xml
- ${PWD}/fs/tmp/var/lib/clickhouse/:/var/lib/clickhouse/
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
- ${PWD}/../../../deploy/common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
ports:
- '127.0.0.1:8123:8123'
- '127.0.0.1:9000:9000'
@@ -41,10 +22,7 @@ services:
timeout: 5s
retries: 3
depends_on:
init-clickhouse:
condition: service_completed_successfully
zookeeper:
condition: service_healthy
- zookeeper
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
zookeeper:

View File

@@ -44,5 +44,4 @@
<shard>01</shard>
<replica>01</replica>
</macros>
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
</clickhouse>

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

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

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

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

View File

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

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable sonarjs/cognitive-complexity */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
@@ -221,7 +222,7 @@ function LogDetailInner({
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (): void => {
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -234,7 +235,9 @@ function LogDetailInner({
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
event: e,
});
};
const handleQueryExpressionChange = useCallback(

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

@@ -70,8 +70,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
</Tag>
) : undefined
}
onClick={(): void => {
onSelect(option.selection);
onClick={(e): void => {
onSelect(option.selection, e);
}}
data-testid={`alert-type-card-${option.selection}`}
>
@@ -108,7 +108,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
}
interface SelectAlertTypeProps {
onSelect: (typ: AlertTypes) => void;
onSelect: (type: AlertTypes, event?: React.MouseEvent) => void;
}
export default SelectAlertType;

View File

@@ -33,9 +33,9 @@ function Footer(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const handleDiscard = (): void => {
const handleDiscard = (e: React.MouseEvent): void => {
discardAlertRule();
safeNavigate('/alerts');
safeNavigate('/alerts', { event: e });
};
const alertValidationMessage = useMemo(

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

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

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
@@ -330,13 +330,18 @@ function FormAlertRules({
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
const onCancelHandler = useCallback(
(e?: React.MouseEvent) => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
event: e,
});
},
[safeNavigate, urlQuery],
);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults

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

@@ -1,7 +1,13 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import {
LoadingOutlined,
SearchOutlined,
@@ -290,9 +296,9 @@ function FullView({
<Button
className="switch-edit-btn"
disabled={response.isFetching || response.isLoading}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
if (dashboardEditView) {
safeNavigate(dashboardEditView);
safeNavigate(dashboardEditView, { event: e });
}
}}
>

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

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable sonarjs/no-duplicate-string */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
@@ -25,6 +26,7 @@ import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { isIngestionActive } from 'utils/app';
import { navigateToPage } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import AlertRules from './AlertRules/AlertRules';
@@ -370,11 +372,12 @@ export default function Home(): JSX.Element {
role="button"
tabIndex={0}
className="active-ingestion-card-actions"
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
// eslint-disable-next-line sonarjs/no-duplicate-string
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -411,11 +414,11 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -452,11 +455,11 @@ export default function Home(): JSX.Element {
className="active-ingestion-card-actions"
role="button"
tabIndex={0}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Ingestion Active Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER);
navigateToPage(ROUTES.METRICS_EXPLORER, history.push, e);
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
@@ -506,11 +509,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
history.push(ROUTES.LOGS_EXPLORER);
navigateToPage(ROUTES.LOGS_EXPLORER, history.push, e);
}}
>
Open Logs Explorer
@@ -520,11 +523,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
history.push(ROUTES.TRACES_EXPLORER);
navigateToPage(ROUTES.TRACES_EXPLORER, history.push, e);
}}
>
Open Traces Explorer
@@ -534,11 +537,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
navigateToPage(ROUTES.METRICS_EXPLORER_EXPLORER, history.push, e);
}}
>
Open Metrics Explorer
@@ -575,11 +578,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
history.push(ROUTES.ALL_DASHBOARD);
navigateToPage(ROUTES.ALL_DASHBOARD, history.push, e);
}}
>
Create dashboard
@@ -617,11 +620,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(): void => {
onClick={(e: React.MouseEvent): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
history.push(ROUTES.ALERTS_NEW);
navigateToPage(ROUTES.ALERTS_NEW, history.push, e);
}}
>
Create an alert

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { QueryKey } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -117,7 +117,7 @@ const ServicesListTable = memo(
onRowClick,
}: {
services: ServicesList[];
onRowClick: (record: ServicesList) => void;
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
}): JSX.Element => (
<div className="services-list-container home-data-item-container metrics-services-list">
<div className="services-list">
@@ -126,8 +126,8 @@ const ServicesListTable = memo(
dataSource={services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
})}
/>
</div>
@@ -285,11 +285,11 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList) => {
(record: ServicesList, event: React.MouseEvent) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
},
[safeNavigate],
);

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { Link } from 'react-router-dom';
import { Button, Select, Skeleton, Table } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -173,13 +173,13 @@ export default function ServiceTraces({
dataSource={top5Services}
pagination={false}
className="services-table"
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, { event });
},
})}
/>

View File

@@ -11,6 +11,7 @@ import {
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
import {
@@ -162,7 +163,16 @@ export default function HostsListTable({
[],
);
const handleRowClick = (record: HostRowData): void => {
const handleRowClick = (
record: HostRowData,
event: React.MouseEvent,
): void => {
if (isModifierKeyPressed(event)) {
const params = new URLSearchParams(window.location.search);
params.set('hostName', record.hostName);
openInNewTab(`${window.location.pathname}?${params.toString()}`);
return;
}
onHostClick(record.hostName);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.HostEntity,
@@ -235,8 +245,8 @@ export default function HostsListTable({
(record as HostRowData & { key: string }).key ?? record.hostName
}
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record: HostRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
/>

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -24,6 +26,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -450,7 +453,19 @@ function K8sClustersList({
);
}, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]);
const handleRowClick = (record: K8sClustersRowData): void => {
const handleRowClick = (
record: K8sClustersRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
record.clusterUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
@@ -514,8 +529,19 @@ function K8sClustersList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME,
record.clusterUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedClusterName(record.clusterUID);
},
className: 'expanded-clickable-row',
@@ -706,8 +732,10 @@ function K8sClustersList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -25,6 +27,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -456,7 +459,19 @@ function K8sDaemonSetsList({
nestedDaemonSetsData,
]);
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
const handleRowClick = (
record: K8sDaemonSetsRowData,
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
record.daemonsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedDaemonSetUID(record.daemonsetUID);
@@ -520,8 +535,19 @@ function K8sDaemonSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID,
record.daemonsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedDaemonSetUID(record.daemonsetUID);
},
className: 'expanded-clickable-row',
@@ -714,8 +740,10 @@ function K8sDaemonSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -25,6 +27,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -462,7 +465,19 @@ function K8sDeploymentsList({
nestedDeploymentsData,
]);
const handleRowClick = (record: K8sDeploymentsRowData): void => {
const handleRowClick = (
record: K8sDeploymentsRowData,
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
record.deploymentUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
@@ -526,8 +541,19 @@ function K8sDeploymentsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID,
record.deploymentUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedDeploymentUID(record.deploymentUID);
},
className: 'expanded-clickable-row',
@@ -721,8 +747,10 @@ function K8sDeploymentsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -25,6 +27,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -427,7 +430,16 @@ function K8sJobsList({
return jobsData.find((job) => job.jobName === selectedJobUID) || null;
}, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]);
const handleRowClick = (record: K8sJobsRowData): void => {
const handleRowClick = (
record: K8sJobsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedJobUID(record.jobUID);
@@ -491,8 +503,16 @@ function K8sJobsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedJobUID(record.jobUID);
},
className: 'expanded-clickable-row',
@@ -683,8 +703,10 @@ function K8sJobsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -24,6 +26,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -458,7 +461,19 @@ function K8sNamespacesList({
nestedNamespacesData,
]);
const handleRowClick = (record: K8sNamespacesRowData): void => {
const handleRowClick = (
record: K8sNamespacesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
record.namespaceUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
@@ -522,8 +537,19 @@ function K8sNamespacesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
record.namespaceUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedNamespaceUID(record.namespaceUID);
},
className: 'expanded-clickable-row',
@@ -715,8 +741,10 @@ function K8sNamespacesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -24,6 +26,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -437,7 +440,16 @@ function K8sNodesList({
return nodesData.find((node) => node.nodeUID === selectedNodeUID) || null;
}, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]);
const handleRowClick = (record: K8sNodesRowData): void => {
const handleRowClick = (
record: K8sNodesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setSelectedNodeUID(record.nodeUID);
@@ -502,8 +514,19 @@ function K8sNodesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID,
record.nodeUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedNodeUID(record.nodeUID);
},
className: 'expanded-clickable-row',
@@ -694,8 +717,10 @@ function K8sNodesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -27,6 +28,7 @@ import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -495,7 +497,16 @@ function K8sPodsList({
}
}, [selectedRowData, fetchGroupedByRowData]);
const handleRowClick = (record: K8sPodsRowData): void => {
const handleRowClick = (
record: K8sPodsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedPodUID(record.podUID);
setSearchParams({
@@ -615,8 +626,14 @@ function K8sPodsList({
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {
if (isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedPodUID(record.podUID);
},
className: 'expanded-clickable-row',
@@ -752,8 +769,8 @@ function K8sPodsList({
scroll={{ x: true }}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record: K8sPodsRowData): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -25,6 +27,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -459,7 +462,19 @@ function K8sStatefulSetsList({
nestedStatefulSetsData,
]);
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
const handleRowClick = (
record: K8sStatefulSetsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
record.statefulsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
@@ -523,8 +538,19 @@ function K8sStatefulSetsList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
record.statefulsetUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedStatefulSetUID(record.statefulsetUID);
},
className: 'expanded-clickable-row',
@@ -717,8 +743,8 @@ function K8sStatefulSetsList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(record) => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import { LoadingOutlined } from '@ant-design/icons';
import {
@@ -25,6 +27,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
@@ -389,7 +392,16 @@ function K8sVolumesList({
);
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
const handleRowClick = (record: K8sVolumesRowData): void => {
const handleRowClick = (
record: K8sVolumesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedVolumeUID(record.volumeUID);
@@ -453,8 +465,19 @@ function K8sVolumesList({
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
record.volumeUID,
);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setselectedVolumeUID(record.volumeUID);
},
className: 'expanded-clickable-row',
@@ -640,8 +663,10 @@ function K8sVolumesList({
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
onRow={(
record,
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
className: 'clickable-row',
})}
expandable={{

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -7,6 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -36,9 +37,13 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback(() => {
const onClickNewAlertHandler = useCallback((e: React.MouseEvent) => {
setLoading(false);
history.push(ROUTES.ALERTS_NEW);
if (isModifierKeyPressed(e)) {
openInNewTab(ROUTES.ALERTS_NEW);
} else {
history.push(ROUTES.ALERTS_NEW);
}
}, []);
return (

View File

@@ -1,4 +1,5 @@
import { useCallback, useState } from 'react';
/* eslint-disable react/display-name */
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { PlusOutlined } from '@ant-design/icons';
@@ -99,16 +100,22 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
});
}, [notificationsApi, t]);
const onClickNewAlertHandler = useCallback(() => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
const onClickNewAlertHandler = useCallback(
(e: React.MouseEvent): void => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'new',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION, { event: e });
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
[],
);
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
const onEditHandler = (
record: GettableAlert,
options?: { event?: React.MouseEvent; newTab?: boolean },
): void => {
const compositeQuery = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(record.condition.compositeQuery),
record.alertType as AlertTypes,
@@ -124,11 +131,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
setEditLoader(false);
if (openInNewTab) {
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
} else {
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
}
safeNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, {
event: options?.event,
newTab: options?.newTab,
});
};
const onCloneHandler = (
@@ -265,7 +271,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
e.stopPropagation();
e.preventDefault();
onEditHandler(record, e.metaKey || e.ctrlKey);
onEditHandler(record, { event: e });
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
@@ -330,7 +336,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
/>,
<ColumnButton
key="2"
onClick={(): void => onEditHandler(record, false)}
onClick={(e: React.MouseEvent): void =>
onEditHandler(record, { event: e })
}
type="link"
loading={editLoader}
>
@@ -338,7 +346,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</ColumnButton>,
<ColumnButton
key="3"
onClick={(): void => onEditHandler(record, true)}
onClick={(): void => onEditHandler(record, { newTab: true })}
type="link"
loading={editLoader}
>

View File

@@ -372,11 +372,7 @@ function DashboardsList(): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}
safeNavigate(getLink(), { event });
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
dashboardName: dashboard.name,

View File

@@ -32,6 +32,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -236,7 +237,7 @@ function Application(): JSX.Element {
timestamp: number,
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): (() => void) => (): void => {
): ((e: React.MouseEvent) => void) => (e: React.MouseEvent): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - stepInterval);
@@ -260,7 +261,11 @@ function Application(): JSX.Element {
queryString,
);
history.push(newPath);
if (isModifierKeyPressed(e)) {
openInNewTab(newPath);
} else {
history.push(newPath);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stepInterval],

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
@@ -6,9 +7,9 @@ import './GraphControlsPanel.styles.scss';
interface GraphControlsPanelProps {
id: string;
onViewLogsClick?: () => void;
onViewTracesClick: () => void;
onViewAPIMonitoringClick?: () => void;
onViewLogsClick?: (e: React.MouseEvent) => void;
onViewTracesClick: (e: React.MouseEvent) => void;
onViewAPIMonitoringClick?: (e: React.MouseEvent) => void;
}
function GraphControlsPanel({

View File

@@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -42,7 +42,10 @@ interface OnViewTracePopupClickProps {
apmToTraceQuery: Query;
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
safeNavigate: (
url: string,
options?: { event?: React.MouseEvent | MouseEvent },
) => void;
}
interface OnViewAPIMonitoringPopupClickProps {
@@ -51,8 +54,10 @@ interface OnViewAPIMonitoringPopupClickProps {
stepInterval?: number;
domainName: string;
isError: boolean;
safeNavigate: (url: string) => void;
safeNavigate: (
url: string,
options?: { event?: React.MouseEvent | MouseEvent },
) => void;
}
export function generateExplorerPath(
@@ -93,8 +98,8 @@ export function onViewTracePopupClick({
isViewLogsClicked,
stepInterval,
safeNavigate,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
}: OnViewTracePopupClickProps): (e?: React.MouseEvent) => void {
return (e?: React.MouseEvent): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
@@ -118,7 +123,7 @@ export function onViewTracePopupClick({
queryString,
);
safeNavigate(newPath);
safeNavigate(newPath, { event: e });
};
}
@@ -149,8 +154,8 @@ export function onViewAPIMonitoringPopupClick({
isError,
stepInterval,
safeNavigate,
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
return (): void => {
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
return (e?: React.MouseEvent): void => {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
@@ -190,7 +195,7 @@ export function onViewAPIMonitoringPopupClick({
filters,
);
safeNavigate(newPath);
safeNavigate(newPath, { event: e });
};
}

View File

@@ -115,8 +115,9 @@ function MetricsTable({
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
onRow={(record): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void =>
openMetricDetails(record.key, 'list', event),
className: 'clickable-row',
})}
/>

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/* eslint-disable no-nested-ternary */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
@@ -27,6 +28,7 @@ import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import InspectModal from '../Inspect';
@@ -245,7 +247,15 @@ function Summary(): JSX.Element {
const openMetricDetails = (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
const newParams = new URLSearchParams(searchParams);
newParams.set(IS_METRIC_DETAILS_OPEN_KEY, 'true');
newParams.set(SELECTED_METRIC_NAME_KEY, metricName);
openInNewTab(`${window.location.pathname}?${newParams.toString()}`);
return;
}
setSelectedMetricName(metricName);
setIsMetricDetailsOpen(true);
setSearchParams({

View File

@@ -207,7 +207,11 @@ describe('MetricsTable', () => {
);
fireEvent.click(screen.getByText('Metric 1'));
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
expect(mockOpenMetricDetails).toHaveBeenCalledWith(
'metric1',
'list',
expect.any(Object),
);
});
it('calls setOrderBy when column header is clicked', () => {

View File

@@ -18,7 +18,11 @@ export interface MetricsTableProps {
onPaginationChange: (page: number, pageSize: number) => void;
setOrderBy: (orderBy: Querybuildertypesv5OrderByDTO) => void;
totalCount: number;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
queryFilterExpression: Filter;
onFilterChange: (expression: string) => void;
}
@@ -37,7 +41,11 @@ export interface MetricsTreemapProps {
isError: boolean;
error?: APIError;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
}
@@ -47,7 +55,11 @@ export interface MetricsTreemapInternalProps {
error?: APIError;
data: MetricsexplorertypesTreemapResponseDTO | undefined;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
openMetricDetails: (
metricName: string,
view: 'list' | 'treemap',
event?: React.MouseEvent,
) => void;
}
export interface OrderByPayload {

View File

@@ -1,9 +1,10 @@
import { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Badge, Button } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Undo } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { buttonText, RIBBON_STYLES } from './config';
@@ -21,23 +22,30 @@ function NewExplorerCTA(): JSX.Element | null {
[location.pathname],
);
const onClickHandler = useCallback((): void => {
if (location.pathname === ROUTES.LOGS_EXPLORER) {
history.push(ROUTES.OLD_LOGS_EXPLORER);
} else if (location.pathname === ROUTES.TRACE) {
history.push(ROUTES.TRACES_EXPLORER);
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
history.push(ROUTES.LOGS_EXPLORER);
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
history.push(ROUTES.TRACE);
}
}, [location.pathname]);
const onClickHandler = useCallback(
(e?: React.MouseEvent): void => {
let targetPath: string;
if (location.pathname === ROUTES.LOGS_EXPLORER) {
targetPath = ROUTES.OLD_LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACE) {
targetPath = ROUTES.TRACES_EXPLORER;
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
targetPath = ROUTES.LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
targetPath = ROUTES.TRACE;
} else {
return;
}
navigateToPage(targetPath, history.push, e);
},
[location.pathname],
);
const button = useMemo(
() => (
<Button
icon={<Undo size={16} />}
onClick={onClickHandler}
onClick={(e): void => onClickHandler(e)}
data-testid="newExplorerCTA"
type="text"
className="periscope-btn link"

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

@@ -1,4 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useEffectOnce } from 'react-use';
@@ -15,6 +17,7 @@ import { InviteMemberFormValues } from 'container/OrganizationSettings/utils';
import history from 'lib/history';
import { UserPlus } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { navigateToPage } from 'utils/navigation';
import ModuleStepsContainer from './common/ModuleStepsContainer/ModuleStepsContainer';
import { stepsMap } from './constants/stepsConfig';
@@ -250,9 +253,13 @@ export default function Onboarding(): JSX.Element {
}
};
const handleNext = (): void => {
const handleNext = (e?: React.MouseEvent): void => {
if (activeStep <= 3) {
history.push(moduleRouteMap[selectedModule.id as ModulesMap]);
navigateToPage(
moduleRouteMap[selectedModule.id as ModulesMap],
history.push,
e,
);
}
};
@@ -315,9 +322,9 @@ export default function Onboarding(): JSX.Element {
{activeStep === 1 && (
<div className="onboarding-page">
<div
onClick={(): void => {
onClick={(e): void => {
logEvent('Onboarding V2: Skip Button Clicked', {});
history.push(ROUTES.APPLICATION);
navigateToPage(ROUTES.APPLICATION, history.push, e);
}}
className="skip-to-console"
>
@@ -353,7 +360,11 @@ export default function Onboarding(): JSX.Element {
</div>
</div>
<div className="continue-to-next-step">
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
<Button
type="primary"
icon={<ArrowRightOutlined />}
onClick={(e): void => handleNext(e)}
>
{t('get_started')}
</Button>
</div>
@@ -384,17 +395,16 @@ export default function Onboarding(): JSX.Element {
{activeStep > 1 && (
<div className="stepsContainer">
<ModuleStepsContainer
onReselectModule={(): void => {
onReselectModule={(e?: React.MouseEvent): void => {
setCurrent(current - 1);
setActiveStep(activeStep - 1);
setSelectedModule(useCases.APM);
resetProgress();
if (isOnboardingV3Enabled) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
history.push(ROUTES.GET_STARTED);
}
const path = isOnboardingV3Enabled
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
navigateToPage(path, history.push, e);
}}
selectedModule={selectedModule}
selectedModuleSteps={selectedModuleSteps}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { LoadingOutlined } from '@ant-design/icons';
@@ -21,6 +21,7 @@ import {
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import './DataSource.styles.scss';
@@ -139,13 +140,13 @@ export default function DataSource(): JSX.Element {
}
};
const goToIntegrationsPage = (): void => {
const goToIntegrationsPage = (e?: React.MouseEvent): void => {
logEvent('Onboarding V2: Go to integrations', {
module: selectedModule?.id,
dataSource: selectedDataSource?.name,
framework: selectedFramework,
});
history.push(ROUTES.INTEGRATIONS);
navigateToPage(ROUTES.INTEGRATIONS, history.push, e);
};
return (
@@ -247,7 +248,7 @@ export default function DataSource(): JSX.Element {
page which allows more sources of sending data
</Typography.Text>
<Button
onClick={goToIntegrationsPage}
onClick={(e): void => goToIntegrationsPage(e)}
icon={<Blocks size={14} />}
className="navigate-integrations-page-btn"
>

View File

@@ -1,4 +1,4 @@
import { SetStateAction, useState } from 'react';
import React, { SetStateAction, useState } from 'react';
import {
ArrowLeftOutlined,
ArrowRightOutlined,
@@ -15,6 +15,7 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { UserPlus } from 'lucide-react';
import { navigateToPage } from 'utils/navigation';
import { useOnboardingContext } from '../../context/OnboardingContext';
import {
@@ -130,7 +131,7 @@ export default function ModuleStepsContainer({
);
};
const redirectToModules = (): void => {
const redirectToModules = (event?: React.MouseEvent): void => {
logEvent('Onboarding V2 Complete', {
module: selectedModule.id,
dataSource: selectedDataSource?.id,
@@ -140,26 +141,28 @@ export default function ModuleStepsContainer({
serviceName,
});
let targetPath: string;
if (selectedModule.id === ModulesMap.APM) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else if (selectedModule.id === ModulesMap.LogsManagement) {
history.push(ROUTES.LOGS_EXPLORER);
targetPath = ROUTES.LOGS_EXPLORER;
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else if (selectedModule.id === ModulesMap.AwsMonitoring) {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
} else {
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
}
navigateToPage(targetPath, history.push, event);
};
const handleNext = (): void => {
const handleNext = (event?: React.MouseEvent): void => {
const isValid = isValidForm();
if (isValid) {
if (current === lastStepIndex) {
resetProgress();
redirectToModules();
redirectToModules(event);
return;
}
@@ -367,8 +370,8 @@ export default function ModuleStepsContainer({
}
};
const handleLogoClick = (): void => {
history.push('/home');
const handleLogoClick = (e: React.MouseEvent): void => {
navigateToPage('/home', history.push, e);
};
return (
@@ -388,7 +391,7 @@ export default function ModuleStepsContainer({
style={{ display: 'flex', alignItems: 'center' }}
type="default"
icon={<LeftCircleOutlined />}
onClick={onReselectModule}
onClick={(e): void => onReselectModule(e)}
>
{selectedModule.title}
</Button>
@@ -458,7 +461,11 @@ export default function ModuleStepsContainer({
>
Back
</Button>
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
<Button
onClick={(e): void => handleNext(e)}
type="primary"
icon={<ArrowRightOutlined />}
>
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button>
<LaunchChatSupport

View File

@@ -21,6 +21,7 @@ import history from 'lib/history';
import { isEmpty } from 'lodash-es';
import { CheckIcon, Goal, UserPlus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { navigateToPage } from 'utils/navigation';
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
import InviteTeamMembers from '../InviteTeamMembers/InviteTeamMembers';
@@ -413,7 +414,10 @@ function OnboardingAddDataSource(): JSX.Element {
]);
}, [org]);
const handleUpdateCurrentStep = (step: number): void => {
const handleUpdateCurrentStep = (
step: number,
event?: React.MouseEvent,
): void => {
setCurrentStep(step);
if (step === 1) {
@@ -443,43 +447,45 @@ function OnboardingAddDataSource(): JSX.Element {
...setupStepItemsBase.slice(2),
]);
} else if (step === 3) {
let targetPath: string;
switch (selectedDataSource?.module) {
case 'apm':
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
break;
case 'logs':
history.push(ROUTES.LOGS);
targetPath = ROUTES.LOGS;
break;
case 'metrics':
history.push(ROUTES.METRICS_EXPLORER);
targetPath = ROUTES.METRICS_EXPLORER;
break;
case 'dashboards':
history.push(ROUTES.ALL_DASHBOARD);
targetPath = ROUTES.ALL_DASHBOARD;
break;
case 'infra-monitoring-hosts':
history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS);
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
break;
case 'infra-monitoring-k8s':
history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES);
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
break;
case 'messaging-queues-kafka':
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
break;
case 'messaging-queues-celery':
history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK);
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
break;
case 'integrations':
history.push(ROUTES.INTEGRATIONS);
targetPath = ROUTES.INTEGRATIONS;
break;
case 'home':
history.push(ROUTES.HOME);
targetPath = ROUTES.HOME;
break;
case 'api-monitoring':
history.push(ROUTES.API_MONITORING);
targetPath = ROUTES.API_MONITORING;
break;
default:
history.push(ROUTES.APPLICATION);
targetPath = ROUTES.APPLICATION;
}
navigateToPage(targetPath, history.push, event);
}
};
@@ -628,7 +634,7 @@ function OnboardingAddDataSource(): JSX.Element {
<X
size={14}
className="onboarding-header-container-close-icon"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
{
@@ -636,7 +642,7 @@ function OnboardingAddDataSource(): JSX.Element {
},
);
history.push(ROUTES.HOME);
navigateToPage(ROUTES.HOME, history.push, e);
}}
/>
<Typography.Text>Get Started (2/4)</Typography.Text>
@@ -963,7 +969,7 @@ function OnboardingAddDataSource(): JSX.Element {
type="primary"
disabled={!selectedDataSource}
shape="round"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
{
@@ -977,7 +983,7 @@ function OnboardingAddDataSource(): JSX.Element {
selectedEnvironment || selectedFramework || selectedDataSource;
if (currentEntity?.internalRedirect && currentEntity?.link) {
history.push(currentEntity.link);
navigateToPage(currentEntity.link, history.push, e);
} else {
handleUpdateCurrentStep(2);
}
@@ -1048,7 +1054,7 @@ function OnboardingAddDataSource(): JSX.Element {
<Button
type="primary"
shape="round"
onClick={(): void => {
onClick={(e): void => {
logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
{
@@ -1060,7 +1066,7 @@ function OnboardingAddDataSource(): JSX.Element {
);
handleFilterByCategory('All');
handleUpdateCurrentStep(3);
handleUpdateCurrentStep(3, e);
}}
>
Continue

View File

@@ -63,6 +63,7 @@ import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config';
@@ -305,8 +306,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
icon: <Cog size={16} />,
};
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const [
@@ -435,10 +434,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
const onClickGetStarted = (event: MouseEvent): void => {
logEvent('Sidebar: Menu clicked', {
menuRoute: '/get-started',
@@ -449,7 +444,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
if (isCtrlMetaKey(event)) {
if (isModifierKeyPressed(event)) {
openInNewTab(onboaringRoute);
} else {
history.push(onboaringRoute);
@@ -464,7 +459,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlMetaKey(event)) {
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
@@ -627,7 +622,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
if (item.key === ROUTES.SETTINGS) {
if (isCtrlMetaKey(event)) {
if (isModifierKeyPressed(event)) {
openInNewTab(settingsRoute);
} else {
history.push(settingsRoute);
@@ -805,6 +800,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleHelpSupportMenuItemClick = (info: SidebarItem): void => {
const item = helpSupportDropdownMenuItems.find(
(item) => !('type' in item) && item.key === info.key,
@@ -814,6 +810,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
window.open(item.url, '_blank');
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
if (item && !('type' in item)) {
logEvent('Help Popover: Item clicked', {
menuRoute: item.key,
@@ -821,8 +819,19 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
});
switch (item.key) {
case ROUTES.SHORTCUTS:
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SHORTCUTS);
} else {
history.push(ROUTES.SHORTCUTS);
}
break;
case 'invite-collaborators':
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
} else {
history.push(`${ROUTES.ORG_SETTINGS}#invite-team-members`);
}
break;
case 'chat-support':
if (window.pylon) {
@@ -839,6 +848,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleSettingsMenuItemClick = (info: SidebarItem): void => {
const item = (userSettingsDropdownMenuItems ?? []).find(
(item) => item?.key === info.key,
@@ -856,15 +866,30 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
menuRoute: item?.key,
menuLabel,
});
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
switch (info.key) {
case 'account':
history.push(ROUTES.MY_SETTINGS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.MY_SETTINGS);
} else {
history.push(ROUTES.MY_SETTINGS);
}
break;
case 'workspace':
history.push(ROUTES.SETTINGS);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.SETTINGS);
} else {
history.push(ROUTES.SETTINGS);
}
break;
case 'license':
history.push(ROUTES.LIST_LICENSES);
if (event && isModifierKeyPressed(event)) {
openInNewTab(ROUTES.LIST_LICENSES);
} else {
history.push(ROUTES.LIST_LICENSES);
}
break;
case 'keyboard-shortcuts':
history.push(ROUTES.SHORTCUTS);

View File

@@ -0,0 +1,106 @@
/**
* Tests for useSafeNavigate's mock contract.
*
* The real useSafeNavigate hook is globally replaced by a mock via
* jest.config.ts moduleNameMapper, so we cannot test the real
* implementation here. Instead we verify:
*
* 1. The mock accepts the new `event` and `newTab` options without
* type errors — ensuring component tests that pass these options
* won't break.
* 2. The shouldOpenNewTab decision logic (extracted inline below)
* matches the real hook's behaviour.
*/
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isModifierKeyPressed } from 'utils/navigation';
/**
* Mirrors the shouldOpenNewTab logic from the real useSafeNavigate hook.
* Kept in sync manually — any drift will be caught by integration tests.
*/
interface NavigateOptions {
newTab?: boolean;
event?: MouseEvent | React.MouseEvent;
}
const shouldOpenNewTab = (options?: NavigateOptions): boolean =>
Boolean(
options?.newTab || (options?.event && isModifierKeyPressed(options.event)),
);
describe('useSafeNavigate mock contract', () => {
it('mock returns a safeNavigate function', () => {
const { safeNavigate } = useSafeNavigate();
expect(typeof safeNavigate).toBe('function');
});
it('safeNavigate accepts string path with event option', () => {
const { safeNavigate } = useSafeNavigate();
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
expect(() => {
safeNavigate('/alerts', { event });
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith('/alerts', { event });
});
it('safeNavigate accepts string path with newTab option', () => {
const { safeNavigate } = useSafeNavigate();
expect(() => {
safeNavigate('/dashboard', { newTab: true });
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith('/dashboard', { newTab: true });
});
it('safeNavigate accepts SafeNavigateParams with event option', () => {
const { safeNavigate } = useSafeNavigate();
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
expect(() => {
safeNavigate({ pathname: '/settings', search: '?tab=general' }, { event });
}).not.toThrow();
expect(safeNavigate).toHaveBeenCalledWith(
{ pathname: '/settings', search: '?tab=general' },
{ event },
);
});
});
describe('shouldOpenNewTab decision logic', () => {
it('returns true when newTab is true', () => {
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
});
it('returns true when event has metaKey pressed', () => {
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
expect(shouldOpenNewTab({ event })).toBe(true);
});
it('returns true when event has ctrlKey pressed', () => {
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
expect(shouldOpenNewTab({ event })).toBe(true);
});
it('returns false when event has no modifier keys', () => {
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
expect(shouldOpenNewTab({ event })).toBe(false);
});
it('returns false when no options provided', () => {
expect(shouldOpenNewTab()).toBe(false);
expect(shouldOpenNewTab(undefined)).toBe(false);
});
it('returns false when options provided without event or newTab', () => {
expect(shouldOpenNewTab({})).toBe(false);
});
it('newTab takes precedence even without event', () => {
expect(shouldOpenNewTab({ newTab: true })).toBe(true);
});
});

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,11 +1,13 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { cloneDeep, isEqual } from 'lodash-es';
import { isModifierKeyPressed } from 'utils/navigation';
interface NavigateOptions {
replace?: boolean;
state?: any;
newTab?: boolean;
event?: MouseEvent | React.MouseEvent;
}
interface SafeNavigateParams {
@@ -105,6 +107,7 @@ export const useSafeNavigate = (
const location = useLocation();
const safeNavigate = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
const currentUrl = new URL(
`${location.pathname}${location.search}`,
@@ -122,8 +125,10 @@ export const useSafeNavigate = (
);
}
// If newTab is true, open in new tab and return early
if (options?.newTab) {
const shouldOpenNewTab =
options?.newTab || (options?.event && isModifierKeyPressed(options.event));
if (shouldOpenNewTab) {
const targetPath =
typeof to === 'string'
? to

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

@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Breadcrumb, Button, Divider } from 'antd';
@@ -18,6 +18,7 @@ import {
NEW_ALERT_SCHEMA_VERSION,
PostableAlertRuleV2,
} from 'types/api/alerts/alertTypesV2';
import { navigateToPage } from 'utils/navigation';
import AlertHeader from './AlertHeader/AlertHeader';
import AlertNotFound from './AlertNotFound';
@@ -58,11 +59,11 @@ function BreadCrumbItem({
if (isLast) {
return <div className="breadcrumb-item breadcrumb-item--last">{title}</div>;
}
const handleNavigate = (): void => {
const handleNavigate = (e: React.MouseEvent): void => {
if (!route) {
return;
}
history.push(ROUTES.LIST_ALL_ALERT);
navigateToPage(ROUTES.LIST_ALL_ALERT, history.push, e);
};
return (

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { Button, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -15,8 +16,8 @@ function AlertNotFound({ isTestAlert }: AlertNotFoundProps): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { safeNavigate } = useSafeNavigate();
const checkAllRulesHandler = (): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT);
const checkAllRulesHandler = (e?: React.MouseEvent): void => {
safeNavigate(ROUTES.LIST_ALL_ALERT, { event: e });
};
const contactSupportHandler = (): void => {

View File

@@ -69,7 +69,10 @@ describe('AlertNotFound', () => {
const user = userEvent.setup();
render(<AlertNotFound isTestAlert={false} />);
await user.click(screen.getByText('Check all rules'));
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.LIST_ALL_ALERT);
expect(mockSafeNavigate).toHaveBeenCalledWith(
ROUTES.LIST_ALL_ALERT,
expect.objectContaining({ event: expect.any(Object) }),
);
});
it('should navigate to the correct support page for cloud users when button is clicked', async () => {

View File

@@ -18,7 +18,7 @@ function AlertTypeSelectionPage(): JSX.Element {
}, []);
const handleSelectType = useCallback(
(type: AlertTypes): void => {
(type: AlertTypes, event?: React.MouseEvent): void => {
// For anamoly based alert, we need to set the ruleType to anomaly_rule
// and alertType to metrics_based_alert
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
@@ -41,7 +41,7 @@ function AlertTypeSelectionPage(): JSX.Element {
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
}
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`, { event });
},
[queryParams, safeNavigate],
);

View File

@@ -1,14 +1,14 @@
import { useHistory } from 'react-router-dom';
import { Button, Flex, Typography } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ArrowRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { navigateToPage } from 'utils/navigation';
import { routePermission } from 'utils/permission';
import './Integrations.styles.scss';
function Header(): JSX.Element {
const history = useHistory();
const { user } = useAppContext();
const isGetStartedWithCloudAllowed = routePermission.GET_STARTED_WITH_CLOUD.includes(
@@ -30,7 +30,9 @@ function Header(): JSX.Element {
<Button
className="periscope-btn primary view-data-sources-btn"
type="primary"
onClick={(): void => history.push(ROUTES.GET_STARTED_WITH_CLOUD)}
onClick={(e): void =>
navigateToPage(ROUTES.GET_STARTED_WITH_CLOUD, history.push, e)
}
>
<span>View 150+ Data Sources</span>
<ArrowRight size={14} />

View File

@@ -6,6 +6,7 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import useUrlQuery from 'hooks/useUrlQuery';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import {
MessagingQueuesViewType,
@@ -63,8 +64,14 @@ function MQDetailPage(): JSX.Element {
selectedView !== MessagingQueuesViewType.dropRate.value &&
selectedView !== MessagingQueuesViewType.metricPage.value;
const handleBackClick = (): void => {
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
const handleBackClick = (
event?: React.MouseEvent | React.KeyboardEvent,
): void => {
if (event && isModifierKeyPressed(event as React.MouseEvent)) {
openInNewTab(ROUTES.MESSAGING_QUEUES_KAFKA);
} else {
history.push(ROUTES.MESSAGING_QUEUES_KAFKA);
}
};
return (
@@ -76,7 +83,7 @@ function MQDetailPage(): JSX.Element {
className="message-queue-text"
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleBackClick();
handleBackClick(e);
}
}}
role="button"

View File

@@ -28,6 +28,7 @@ import {
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { formatNumericValue } from 'utils/numericUtils';
import { getTableDataForProducerLatencyOverview } from './MQTableUtils';
@@ -36,6 +37,7 @@ import './MQTables.styles.scss';
const INITIAL_PAGE_SIZE = 10;
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
@@ -77,7 +79,12 @@ export function getColumns(
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
history.push(`/services/${encodeURIComponent(text)}`);
const path = `/services/${encodeURIComponent(text)}`;
if (isModifierKeyPressed(e)) {
openInNewTab(path);
} else {
history.push(path);
}
}}
>
{text}

View File

@@ -9,6 +9,7 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import {
KAFKA_SETUP_DOC_LINK,
@@ -22,26 +23,40 @@ function MessagingQueues(): JSX.Element {
const history = useHistory();
const { t } = useTranslation('messagingQueuesKafkaOverview');
const redirectToDetailsPage = (callerView?: string): void => {
const redirectToDetailsPage = (
callerView?: string,
event?: React.MouseEvent,
): void => {
logEvent('Messaging Queues: View details clicked', {
page: 'Messaging Queues Overview',
source: callerView,
});
history.push(
`${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
);
const path = `${ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL}?${QueryParams.mqServiceView}=${callerView}`;
if (event && isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
history.push(path);
}
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const getStartedRedirect = (link: string, sourceCard: string): void => {
const getStartedRedirect = (
link: string,
sourceCard: string,
event?: React.MouseEvent,
): void => {
logEvent('Messaging Queues: Get started clicked', {
source: sourceCard,
link: isCloudUserVal ? link : KAFKA_SETUP_DOC_LINK,
});
if (isCloudUserVal) {
history.push(link);
if (event && isModifierKeyPressed(event)) {
openInNewTab(link);
} else {
history.push(link);
}
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
@@ -78,10 +93,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
onClick={(e): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer',
e,
)
}
>
@@ -97,10 +113,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
onClick={(e): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer',
e,
)
}
>
@@ -116,10 +133,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
onClick={(e): void =>
getStartedRedirect(
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka',
e,
)
}
>
@@ -142,8 +160,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value, e)
}
>
{t('summarySection.viewDetailsButton')}
@@ -160,8 +178,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value, e)
}
>
{t('summarySection.viewDetailsButton')}
@@ -178,8 +196,11 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
onClick={(e): void =>
redirectToDetailsPage(
MessagingQueuesViewType.partitionLatency.value,
e,
)
}
>
{t('summarySection.viewDetailsButton')}
@@ -196,8 +217,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value, e)
}
>
{t('summarySection.viewDetailsButton')}
@@ -214,8 +235,8 @@ function MessagingQueues(): JSX.Element {
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
onClick={(e): void =>
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value, e)
}
>
{t('summarySection.viewDetailsButton')}

View File

@@ -16,6 +16,7 @@ import history from 'lib/history';
import { Cog } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed, openInNewTab } from 'utils/navigation';
import { getRoutes } from './utils';
@@ -190,12 +191,6 @@ function SettingsPage(): JSX.Element {
],
);
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
const onClickHandler = useCallback(
(key: string, event: MouseEvent | null) => {
const params = new URLSearchParams(search);
@@ -204,7 +199,7 @@ function SettingsPage(): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlMetaKey(event)) {
if (event && isModifierKeyPressed(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect } from 'react';
/* eslint-disable react/no-unescaped-entities */
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import type { TabsProps } from 'antd';
@@ -26,6 +27,7 @@ import { CircleArrowRight } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { navigateToPage } from 'utils/navigation';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@@ -131,10 +133,10 @@ export default function WorkspaceBlocked(): JSX.Element {
});
};
const handleViewBilling = (): void => {
const handleViewBilling = (e?: React.MouseEvent): void => {
logEvent('Workspace Blocked: User Clicked View Billing', {});
history.push(ROUTES.BILLING);
navigateToPage(ROUTES.BILLING, history.push, e);
};
const renderCustomerStories = (
@@ -294,7 +296,7 @@ export default function WorkspaceBlocked(): JSX.Element {
type="link"
size="small"
role="button"
onClick={handleViewBilling}
onClick={(e): void => handleViewBilling(e)}
>
View Billing
</Button>

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

@@ -0,0 +1,137 @@
import {
isModifierKeyPressed,
navigateToPage,
openInNewTab,
} from '../navigation';
describe('navigation utilities', () => {
const originalWindowOpen = window.open;
beforeEach(() => {
window.open = jest.fn();
});
afterEach(() => {
window.open = originalWindowOpen;
});
describe('isModifierKeyPressed', () => {
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
({
metaKey: false,
ctrlKey: false,
button: 0,
...overrides,
} as MouseEvent);
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
const event = createMouseEvent({ metaKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
const event = createMouseEvent({ ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns true when both metaKey and ctrlKey are pressed', () => {
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns false when neither modifier key is pressed', () => {
const event = createMouseEvent();
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns false when only shiftKey or altKey are pressed', () => {
const event = createMouseEvent({
shiftKey: true,
altKey: true,
} as Partial<MouseEvent>);
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns true when middle mouse button is used', () => {
const event = createMouseEvent({ button: 1 });
expect(isModifierKeyPressed(event)).toBe(true);
});
});
describe('openInNewTab', () => {
it('calls window.open with the given path and _blank target', () => {
openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
});
it('handles full URLs', () => {
openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('handles paths with query strings', () => {
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
});
describe('navigateToPage', () => {
const mockNavigate = jest.fn() as jest.MockedFunction<(path: string) => void>;
beforeEach(() => {
mockNavigate.mockClear();
});
it('opens new tab when metaKey is pressed', () => {
const event = { metaKey: true, ctrlKey: false } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
expect(mockNavigate).not.toHaveBeenCalled();
});
it('opens new tab when ctrlKey is pressed', () => {
const event = { metaKey: false, ctrlKey: true } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
expect(mockNavigate).not.toHaveBeenCalled();
});
it('opens new tab when middle mouse button is used', () => {
const event = { metaKey: false, ctrlKey: false, button: 1 } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(window.open).toHaveBeenCalledWith('/services', '_blank');
expect(mockNavigate).not.toHaveBeenCalled();
});
it('calls navigate callback when no modifier key is pressed', () => {
const event = { metaKey: false, ctrlKey: false } as MouseEvent;
navigateToPage('/services', mockNavigate, event);
expect(mockNavigate).toHaveBeenCalledWith('/services');
expect(window.open).not.toHaveBeenCalled();
});
it('calls navigate callback when no event is provided', () => {
navigateToPage('/services', mockNavigate);
expect(mockNavigate).toHaveBeenCalledWith('/services');
expect(window.open).not.toHaveBeenCalled();
});
it('calls navigate callback when event is undefined', () => {
navigateToPage('/dashboard', mockNavigate, undefined);
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
expect(window.open).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,38 @@
import React from 'react';
/**
* Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux)
* during a click event, or if the middle mouse button is used —
* the universal "open in new tab" modifiers.
*/
export const isModifierKeyPressed = (
event: MouseEvent | React.MouseEvent,
): boolean => event.metaKey || event.ctrlKey || event.button === 1;
/**
* Opens the given path in a new browser tab.
*/
export const openInNewTab = (path: string): void => {
window.open(path, '_blank');
};
/**
* Navigates to a path, respecting modifier keys. If Cmd/Ctrl is held,
* the path is opened in a new tab. Otherwise, the provided `navigate`
* callback is invoked for SPA navigation.
*
* @param path - The target URL path
* @param navigate - SPA navigation callback (e.g. history.push, safeNavigate)
* @param event - Optional mouse event to check for modifier keys
*/
export const navigateToPage = (
path: string,
navigate: (path: string) => void,
event?: MouseEvent | React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openInNewTab(path);
} else {
navigate(path);
}
};

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

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

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