Compare commits

..

7 Commits

Author SHA1 Message Date
Nikhil Soni
432723335f fix: remove module doc from skills
These file could go stale leading to wrong info
to the agents
2026-03-02 18:31:38 +05:30
Nikhil Soni
32110e7718 Remove non standard attribute usage 2026-02-20 00:16:29 +05:30
Nikhil Soni
c295ef386d chore(agent): merge and compact traces skill into single reference doc
Combines trace-detail-architecture.md and TRACES_MODULE.md into one
concise traces-module.md (196 lines, down from 1119 combined).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:56:20 +05:30
Nikhil Soni
bf0394cc28 chore(agent): add clickhouse-query skill, project settings, and update existing skills
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 18:27:12 +05:30
Nikhil Soni
fa08ca2fac chore(agent): add skill to code review 2026-02-17 14:08:58 +05:30
Nikhil Soni
08c53fe7e8 docs: add few modules implemtation details
Generated by claude code
2026-01-27 22:33:49 +05:30
Nikhil Soni
c1fac00d2e feat: add claude.md and github commands 2026-01-27 22:33:12 +05:30
61 changed files with 843 additions and 1261 deletions

136
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,136 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
SigNoz is an open-source observability platform (APM, logs, metrics, traces) built on OpenTelemetry and ClickHouse. It provides a unified solution for monitoring applications with features including distributed tracing, log management, metrics dashboards, and alerting.
## Build and Development Commands
### Development Environment Setup
```bash
make devenv-up # Start ClickHouse and OTel Collector for local dev
make devenv-clickhouse # Start only ClickHouse
make devenv-signoz-otel-collector # Start only OTel Collector
make devenv-clickhouse-clean # Clean ClickHouse data
```
### Backend (Go)
```bash
make go-run-community # Run community backend server
make go-run-enterprise # Run enterprise backend server
make go-test # Run all Go unit tests
go test -race ./pkg/... # Run tests for specific package
go test -race ./pkg/querier/... # Example: run querier tests
```
### Integration Tests (Python)
```bash
cd tests/integration
uv sync # Install dependencies
make py-test-setup # Start test environment (keep running with --reuse)
make py-test # Run all integration tests
make py-test-teardown # Stop test environment
# Run specific test
uv run pytest --basetemp=./tmp/ -vv --reuse src/<suite>/<file>.py::test_name
```
### Code Quality
```bash
# Go linting (golangci-lint)
golangci-lint run
# Python formatting/linting
make py-fmt # Format with black
make py-lint # Run isort, autoflake, pylint
```
### OpenAPI Generation
```bash
go run cmd/enterprise/*.go generate openapi
```
## Architecture Overview
### Backend Structure
The Go backend follows a **provider pattern** for dependency injection:
- **`pkg/signoz/`** - IoC container that wires all providers together
- **`pkg/modules/`** - Business logic modules (user, organization, dashboard, etc.)
- **`pkg/<provider>/`** - Provider implementations following consistent structure:
- `<name>.go` - Interface definition
- `config.go` - Configuration (implements `factory.Config`)
- `<implname><name>/provider.go` - Implementation
- `<name>test/` - Mock implementations for testing
### Key Packages
- **`pkg/querier/`** - Query engine for telemetry data (logs, traces, metrics)
- **`pkg/telemetrystore/`** - ClickHouse telemetry storage interface
- **`pkg/sqlstore/`** - Relational database (SQLite/PostgreSQL) for metadata
- **`pkg/apiserver/`** - HTTP API server with OpenAPI integration
- **`pkg/alertmanager/`** - Alert management
- **`pkg/authn/`, `pkg/authz/`** - Authentication and authorization
- **`pkg/flagger/`** - Feature flags (OpenFeature-based)
- **`pkg/errors/`** - Structured error handling
### Enterprise vs Community
- **`cmd/community/`** - Community edition entry point
- **`cmd/enterprise/`** - Enterprise edition entry point
- **`ee/`** - Enterprise-only features
## Code Conventions
### Error Handling
Use the custom `pkg/errors` package instead of standard library:
```go
errors.New(typ, code, message) // Instead of errors.New()
errors.Newf(typ, code, message, args...) // Instead of fmt.Errorf()
errors.Wrapf(err, typ, code, msg) // Wrap with context
```
Define domain-specific error codes:
```go
var CodeThingNotFound = errors.MustNewCode("thing_not_found")
```
### HTTP Handlers
Handlers are thin adapters in modules that:
1. Extract auth context from request
2. Decode request body using `binding` package
3. Call module functions
4. Return responses using `render` package
Register routes in `pkg/apiserver/signozapiserver/` with `handler.New()` and `OpenAPIDef`.
### SQL/Database
- Use Bun ORM via `sqlstore.BunDBCtx(ctx)`
- Star schema with `organizations` as central entity
- All tables have `id`, `created_at`, `updated_at`, `org_id` columns
- Write idempotent migrations in `pkg/sqlmigration/`
- No `ON CASCADE` deletes - handle in application logic
### REST Endpoints
- Use plural resource names: `/v1/organizations`, `/v1/users`
- Use `me` for current user/org: `/v1/organizations/me/users`
- Follow RESTful conventions for CRUD operations
### Linting Rules (from .golangci.yml)
- Don't use `errors` package - use `pkg/errors`
- Don't use `zap` logger - use `slog`
- Don't use `fmt.Errorf` or `fmt.Print*`
## Testing
### Unit Tests
- Run with race detector: `go test -race ./...`
- Provider mocks are in `<provider>test/` packages
### Integration Tests
- Located in `tests/integration/`
- Use pytest with testcontainers
- Files prefixed with numbers for execution order (e.g., `01_database.py`)
- Always use `--reuse` flag during development
- Fixtures in `tests/integration/fixtures/`

View File

@@ -0,0 +1,37 @@
---
name: commit
description: Create a conventional commit with staged changes
allowed-tools: Bash(git commit:*)
---
# Create Conventional Commit
Commit staged changes using conventional commit format: `type(scope): description`
## Types
- `feat:` - New feature
- `fix:` - Bug fix
- `chore:` - Maintenance/refactor/tooling
- `test:` - Tests only
- `docs:` - Documentation
## Process
1. Review staged changes: `git diff --cached`
2. Determine type, optional scope, and description (imperative, <70 chars)
3. Commit using HEREDOC:
```bash
git commit -m "$(cat <<'EOF'
type(scope): description
EOF
)"
```
4. Verify: `git log -1`
## Notes
- Description: imperative mood, lowercase, no period
- Body: explain WHY, not WHAT (code shows what). Keep it concise and brief.
- Do not include co-authored by claude in commit message, we want ownership and accountability to remain with the human contributor.
- Do not automatically add files to stage unless asked to.

View File

@@ -0,0 +1,55 @@
---
name: raise-pr
description: Create a pull request with auto-filled template. Pass 'commit' to commit staged changes first.
allowed-tools: Bash(gh:*, git:*), Read
argument-hint: [commit?]
---
# Raise Pull Request
Create a PR with auto-filled template from commits after origin/main.
## Arguments
- No argument: Create PR with existing commits
- `commit`: Commit staged changes first, then create PR
## Process
1. **If `$ARGUMENTS` is "commit"**: Review staged changes and commit with descriptive message
- Check for staged changes: `git diff --cached --stat`
- If changes exist:
- Review the changes: `git diff --cached`
- Use commit skill for making the commit, i.e. follow conventional commit practices
- Commit command: `git commit -m "message"`
2. **Analyze commits since origin/main**:
- `git log origin/main..HEAD --pretty=format:"%s%n%b"` - get commit messages
- `git diff origin/main...HEAD --stat` - see changes
3. **Read template**: `.github/pull_request_template.md`
4. **Generate PR**:
- **Title**: Short (<70 chars), from commit messages or main change
- **Body**: Fill template sections based on commits/changes, keep these minimal and to the point:
- Summary (why/what/approach) - end with "Closes #<issue_number>" if issue number is available from branch name (git branch --show-current)
- Change Type checkboxes
- Bug Context (if applicable)
- Testing Strategy
- Risk Assessment
- Changelog (if user-facing)
- Checklist
5. **Create PR**:
```bash
git push -u origin $(git branch --show-current)
gh pr create --base main --title "..." --body "..."
gh pr view
```
## Notes
- Analyze ALL commits messages from origin/main to HEAD
- Fill template sections based on commit messages, look into code changes if messages doesn't have all the context.
- Leave template sections as they are if you can't determine the content
- Don't add the changes to git stage, only commit or push whatever user has already staged

View File

@@ -0,0 +1,228 @@
---
name: review
description: Review code changes for bugs, performance issues, and SigNoz convention compliance
allowed-tools: Bash(git:*, gh:*), Read, Glob, Grep
---
# Review Command
Perform a thorough code review following SigNoz's coding conventions and contributing guidelines and for feature intent completion.
## Usage
Invoke this command to review code changes with actionable and concise feedback.
## Process
1. **Determine scope**:
- Ask user what to review if not specified:
- Current git diff (staged or unstaged)
- All changes since origin/main or a commit range
2. **Gather context**:
```bash
# For current changes
git diff --cached # Staged changes
git diff # Unstaged changes
# For commit range
git diff origin/main...HEAD # All changes since main
# for last commit only
git diff HEAD~1..HEAD
```
3. **Read all relevant files thoroughly**:
- Understand the context and purpose of changes
4. **Review against SigNoz guidelines**:
- **Frontend**: Check [Frontend Guidelines](../../frontend/CONTRIBUTIONS.md)
- **Backend/Architecture**: Check [CLAUDE.md](../CLAUDE.md) for provider pattern, error handling, SQL, REST, and linting conventions
- **General**: Check [Contributing Guidelines](../../CONTRIBUTING.md)
- **Commits**: Verify [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
5. **Verify feature intent**:
- Read the PR description, commit message, or linked issue to understand *what* the change claims to do
- Trace the code path end-to-end to confirm the change actually achieves its stated goal
- Check that the happy path works as described
- Identify any scenarios where the feature silently does nothing or produces wrong results
6. **Review for bug introduction**:
- **Regressions**: Does the change break existing behavior? Check callers of modified functions/interfaces
- **Edge cases**: Empty inputs, nil/undefined values, boundary conditions, concurrent access
- **Error paths**: Are all error cases handled? Can errors be swallowed silently?
- **State management**: Are state transitions correct? Can state become inconsistent?
- **Race conditions**: Shared mutable state, async operations, missing locks or guards
- **Type mismatches**: Unsafe casts, implicit conversions, `any` usage hiding real types
7. **Review for performance implications**:
- **Backend**: N+1 queries, missing indexes, unbounded result sets, large allocations in hot paths, unnecessary DB round-trips
- **Frontend**: Unnecessary re-renders from inline objects/functions as props, missing memoization on expensive computations, large bundle imports that should be lazy-loaded, unthrottled event handlers
- **General**: O(n²) or worse algorithms on potentially large datasets, unnecessary network calls, missing pagination or limits
8. **Provide actionable, concise feedback** in structured format
## Review Checklist
For coding conventions and style, refer to the linked guideline docs. This checklist focuses on **review-specific concerns** that guidelines alone don't catch.
### Correctness & Intent
- [ ] Change achieves what the PR/commit/issue describes
- [ ] Happy path works end-to-end
- [ ] Edge cases handled (empty, nil, boundary, concurrent)
- [ ] Error paths don't swallow failures silently
- [ ] No regressions to existing callers of modified code
### Security
- [ ] No exposed secrets, API keys, credentials
- [ ] No sensitive data in logs
- [ ] Input validation at system boundaries
- [ ] Authentication/authorization checked for new endpoints
- [ ] No SQL injection or XSS risks
### Performance
- [ ] No N+1 queries or unbounded result sets
- [ ] No unnecessary re-renders (inline objects/functions as props, missing memoization)
- [ ] No large imports that should be lazy-loaded
- [ ] No O(n²) on potentially large datasets
- [ ] Pagination/limits present where needed
### Testing
- [ ] Edge cases and error paths tested
- [ ] Tests are deterministic (no flakiness)
## Output Format
Provide feedback in this structured format:
```markdown
## Code Review
**Scope**: [What was reviewed]
**Overall**: [1-2 sentence summary and general sentiment]
---
### 🚨 Critical Issues (Must Fix)
1. **[Category]** `file:line`
**Problem**: [What's wrong]
**Why**: [Why it matters]
**Fix**: [Specific solution]
```[language]
// Example fix if helpful
```
### ⚠️ Suggestions (Should Consider)
1. **[Category]** `file:line`
**Issue**: [What could be improved]
**Suggestion**: [Concrete improvement]
---
**References**:
- [Relevant guideline links]
```
## Review Categories
Use these categories for issues:
- **Bug / Regression**: Logic errors, edge cases, race conditions, broken existing behavior
- **Feature Gap**: Change doesn't fully achieve its stated intent
- **Security Risk**: Authentication, authorization, data exposure, injection
- **Performance Issue**: Inefficient queries, unnecessary re-renders, memory leaks, unbounded data
- **Convention Violation**: Style, patterns, architectural guidelines (link to relevant guideline doc)
- **Code Quality**: Complexity, duplication, naming, type safety
- **Testing**: Missing tests, inadequate coverage, flaky tests
## Example Review
```markdown
## Code Review
**Scope**: Changes in `frontend/src/pages/TraceDetail/` (3 files, 245 additions)
**Overall**: Good implementation of pagination feature. Found 2 critical issues and 3 suggestions.
---
### 🚨 Critical Issues (Must Fix)
1. **Security Risk** `TraceList.tsx:45`
**Problem**: API token exposed in client-side code
**Why**: Security vulnerability - tokens should never be in frontend
**Fix**: Move authentication to backend, use session-based auth
2. **Performance Issue** `TraceList.tsx:89`
**Problem**: Inline function passed as prop causes unnecessary re-renders
**Why**: Violates frontend guideline, degrades performance with large lists
**Fix**:
```typescript
const handleTraceClick = useCallback((traceId: string) => {
navigate(`/trace/${traceId}`);
}, [navigate]);
```
### ⚠️ Suggestions (Should Consider)
1. **Code Quality** `TraceList.tsx:120-180`
**Issue**: Function exceeds 40-line guideline
**Suggestion**: Extract into smaller functions:
- `filterTracesByTimeRange()`
- `aggregateMetrics()`
- `renderChartData()`
2. **Type Safety** `types.ts:23`
**Issue**: Using `any` for trace attributes
**Suggestion**: Define proper interface for TraceAttributes
3. **Convention** `TraceList.tsx:12`
**Issue**: File imports not organized
**Suggestion**: Let simple-import-sort auto-organize (will happen on save)
---
**References**:
- [Frontend Guidelines](../../frontend/CONTRIBUTIONS.md)
- [useCallback best practices](https://kentcdodds.com/blog/usememo-and-usecallback)
```
## Tone Guidelines
- **Be respectful**: Focus on code, not the person
- **Be specific**: Always reference exact file:line locations
- **Be concise**: Get to the point, avoid verbosity
- **Be actionable**: Every comment should have clear resolution path
- **Be educational**: Explain why something is an issue, link to guidelines
## Priority Levels
1. **Critical (🚨)**: Security, bugs, data corruption, crashes
2. **Important (⚠️)**: Performance, maintainability, convention violations
3. **Nice to have (💡)**: Style preferences, micro-optimizations
## Important Notes
- **Reference specific guidelines** from docs when applicable
- **Provide code examples** for fixes when helpful
- **Ask questions** if code intent is unclear
- **Link to external resources** for educational value
- **Distinguish** must-fix from should-consider
- **Be concise** - reviewers value their time
## Critical Rules
- **NEVER** be vague - always specify file and line number
- **NEVER** just point out problems - suggest solutions
- **NEVER** review without reading the actual code
- **ALWAYS** check against SigNoz's specific guidelines
- **ALWAYS** provide rationale for each comment
- **ALWAYS** be constructive and respectful
## Reference Documents
- [Frontend Guidelines](../../frontend/CONTRIBUTIONS.md) - React, TypeScript, styling
- [Contributing Guidelines](../../CONTRIBUTING.md) - Workflow, commit conventions
- [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - Commit format
- [CLAUDE.md](../CLAUDE.md) - Project architecture and conventions

View File

@@ -1,7 +1,5 @@
{
"eslint.workingDirectories": [
"./frontend"
],
"eslint.workingDirectories": ["./frontend"],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {

View File

@@ -291,12 +291,3 @@ flagger:
float:
integer:
object:
##################### User #####################
user:
password:
reset:
# Whether to allow users to reset their password themselves.
allow_self: true
# The duration within which a user can reset their password.
max_token_lifetime: 6h

View File

@@ -1985,35 +1985,6 @@ paths:
summary: Update user preference
tags:
- preferences
/api/v2/factor_password/forgot:
post:
deprecated: false
description: This endpoint initiates the forgot password flow by sending a reset
password email
operationId: ForgotPassword
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableForgotPassword'
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
summary: Forgot password
tags:
- users
/api/v2/features:
get:
deprecated: false
@@ -4008,15 +3979,6 @@ components:
token:
type: string
type: object
TypesPostableForgotPassword:
properties:
email:
type: string
frontendBaseURL:
type: string
orgId:
type: string
type: object
TypesPostableInvite:
properties:
email:
@@ -4037,9 +3999,6 @@ components:
type: object
TypesResetPasswordToken:
properties:
expiresAt:
format: date-time
type: string
id:
type: string
passwordId:

View File

@@ -47,7 +47,6 @@ import { AppState } from 'store/reducers';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
@@ -136,7 +135,7 @@ function LogDetailInner({
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (event: React.MouseEvent): void => {
const handleOpenInExplorer = (): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
@@ -149,16 +148,7 @@ function LogDetailInner({
),
),
};
if (isCtrlOrMMetaKey(event)) {
window.open(
`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`,
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
}
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(

View File

@@ -28,7 +28,6 @@ import React, {
useState,
} from 'react';
import { Virtuoso } from 'react-virtuoso';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { popupContainer } from 'utils/selectPopupContainer';
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
@@ -923,7 +922,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const lastVisibleChipIndex = getLastVisibleChipIndex();
// Handle special keyboard combinations
const isCtrlOrCmd = isCtrlOrMMetaKey(e);
const isCtrlOrCmd = e.ctrlKey || e.metaKey;
// Handle Ctrl+A (select all)
if (isCtrlOrCmd && e.key === 'a') {

View File

@@ -5,12 +5,12 @@ import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Channels } from 'types/api/channels/getAll';
import { genericNavigate } from 'utils/genericNavigate';
import Delete from './Delete';
@@ -20,15 +20,13 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { user } = useAppContext();
const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback(
(id: string, event: React.MouseEvent): void => {
genericNavigate(
generatePath(ROUTES.CHANNELS_EDIT, { channelId: id }),
event,
);
},
[],
);
const onClickEditHandler = useCallback((id: string) => {
history.push(
generatePath(ROUTES.CHANNELS_EDIT, {
channelId: id,
}),
);
}, []);
const columns: ColumnsType<Channels> = [
{
@@ -54,10 +52,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
width: 80,
render: (id: string): JSX.Element => (
<>
<Button
onClick={(event: React.MouseEvent): void => onClickEditHandler(id, event)}
type="link"
>
<Button onClick={(): void => onClickEditHandler(id)} type="link">
{t('column_channel_edit')}
</Button>
<Delete id={id} notifications={notifications} />

View File

@@ -8,6 +8,7 @@ import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
@@ -16,7 +17,6 @@ import { useQuery } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { genericNavigate } from 'utils/genericNavigate';
import AlertChannelsComponent from './AlertChannels';
import { Button, ButtonContainer, RightActionContainer } from './styles';
@@ -30,8 +30,8 @@ function AlertChannels(): JSX.Element {
['add_new_channel'],
user.role,
);
const onToggleHandler = useCallback((event: React.MouseEvent) => {
genericNavigate(ROUTES.CHANNELS_NEW, event);
const onToggleHandler = useCallback(() => {
history.push(ROUTES.CHANNELS_NEW);
}, []);
const { isLoading, data, error } = useQuery<
@@ -78,7 +78,7 @@ function AlertChannels(): JSX.Element {
}
>
<Button
onClick={(event: React.MouseEvent): void => onToggleHandler(event)}
onClick={onToggleHandler}
icon={<PlusOutlined />}
disabled={!addNewChannelPermission}
>

View File

@@ -18,7 +18,6 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
import { genericNavigate } from 'utils/genericNavigate';
import { keyToExclude } from './config';
import { DashedContainer, EditorContainer, EventContainer } from './styles';
@@ -112,18 +111,14 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
}));
const onClickTraceHandler = (event: React.MouseEvent): void => {
const onClickTraceHandler = (): void => {
logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail?.groupID,
spanId: errorDetail.spanID,
traceId: errorDetail.traceID,
exceptionId: errorDetail?.errorId,
});
genericNavigate(
`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`,
event,
);
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
};
const logEventCalledRef = useRef(false);
@@ -190,10 +185,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<DashedContainer>
<Typography>{t('see_trace_graph')}</Typography>
<Button
onClick={(event: React.MouseEvent): void => onClickTraceHandler(event)}
type="primary"
>
<Button onClick={onClickTraceHandler} type="primary">
{t('see_error_in_trace_graph')}
</Button>
</DashedContainer>

View File

@@ -73,7 +73,6 @@ import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { panelTypeToExplorerView } from 'utils/explorerUtils';
import { genericNavigate } from 'utils/genericNavigate';
import { PreservedViewsTypes } from './constants';
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
@@ -193,7 +192,7 @@ function ExplorerOptions({
);
const onCreateAlertsHandler = useCallback(
(defaultQuery: Query | null, event?: React.MouseEvent) => {
(defaultQuery: Query | null) => {
if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Create alert', {
panelType,
@@ -212,11 +211,10 @@ function ExplorerOptions({
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
genericNavigate(
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
stringifiedQuery,
)}`,
event,
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -751,9 +749,7 @@ function ExplorerOptions({
<Button
disabled={disabled}
shape="round"
onClick={(event: React.MouseEvent): void =>
onCreateAlertsHandler(query, event)
}
onClick={(): void => onCreateAlertsHandler(query)}
icon={<ConciergeBell size={16} />}
>
Create an Alert

View File

@@ -3,6 +3,7 @@ import getAll from 'api/alerts/getAll';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { ArrowRight, ArrowUpRight, Plus } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
@@ -12,7 +13,6 @@ import { useQuery } from 'react-query';
import { Link, useLocation } from 'react-router-dom';
import { GettableAlert } from 'types/api/alerts/get';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
export default function AlertRules({
onUpdateChecklistDoneItem,
@@ -118,10 +118,7 @@ export default function AlertRules({
</div>
);
const onEditHandler = (
record: GettableAlert,
event?: React.MouseEvent | React.KeyboardEvent,
): void => {
const onEditHandler = (record: GettableAlert) => (): void => {
logEvent('Homepage: Alert clicked', {
ruleId: record.id,
ruleName: record.alert,
@@ -138,7 +135,7 @@ export default function AlertRules({
params.set(QueryParams.ruleId, record.id.toString());
genericNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, event);
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
};
const renderAlertRules = (): JSX.Element => (
@@ -150,10 +147,10 @@ export default function AlertRules({
tabIndex={0}
className="alert-rule-item home-data-item"
key={rule.id}
onClick={(event: React.MouseEvent): void => onEditHandler(rule, event)}
onClick={onEditHandler(rule)}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
onEditHandler(rule, e);
onEditHandler(rule);
}
}}
>

View File

@@ -1,5 +1,4 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable sonarjs/cognitive-complexity */
import { Button, Skeleton, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
@@ -10,7 +9,6 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { genericNavigate } from 'utils/genericNavigate';
import { DOCS_LINKS } from '../constants';
@@ -86,16 +84,16 @@ function DataSourceInfo({
icon={<img src="/Icons/container-plus.svg" alt="plus" />}
role="button"
tabIndex={0}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window.open(
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',

View File

@@ -30,7 +30,6 @@ 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 { genericNavigate } from 'utils/genericNavigate';
import { popupContainer } from 'utils/selectPopupContainer';
import AlertRules from './AlertRules/AlertRules';
@@ -551,11 +550,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Logs',
});
genericNavigate(ROUTES.LOGS_EXPLORER, event);
history.push(ROUTES.LOGS_EXPLORER);
}}
>
Open Logs Explorer
@@ -565,11 +564,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Traces',
});
genericNavigate(ROUTES.TRACES_EXPLORER, event);
history.push(ROUTES.TRACES_EXPLORER);
}}
>
Open Traces Explorer
@@ -579,11 +578,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Wrench size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Metrics',
});
genericNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, event);
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
}}
>
Open Metrics Explorer
@@ -620,11 +619,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Dashboards',
});
genericNavigate(ROUTES.ALL_DASHBOARD, event);
history.push(ROUTES.ALL_DASHBOARD);
}}
>
Create dashboard
@@ -662,11 +661,11 @@ export default function Home(): JSX.Element {
type="default"
className="periscope-btn secondary"
icon={<Plus size={14} />}
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Explore clicked', {
source: 'Alerts',
});
genericNavigate(ROUTES.ALERTS_NEW, event);
history.push(ROUTES.ALERTS_NEW);
}}
>
Create an alert

View File

@@ -4,12 +4,12 @@ import './HomeChecklist.styles.scss';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
export type ChecklistItem = {
id: string;
@@ -86,22 +86,18 @@ function HomeChecklist({
<Button
type="default"
className="periscope-btn secondary"
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Welcome Checklist: Get started clicked', {
step: item.id,
});
const checkForNewTabAndNavigate = (): void => {
genericNavigate(item.toRoute || '', event);
};
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
checkForNewTabAndNavigate();
history.push(item.toRoute || '');
} else if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
checkForNewTabAndNavigate();
history.push(item.toRoute || '');
} else {
window?.open(
item.docsLink || '',

View File

@@ -11,6 +11,7 @@ import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { ArrowRight, ArrowUpRight } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -28,8 +29,6 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
@@ -65,7 +64,7 @@ const EmptyState = memo(
<Button
type="default"
className="periscope-btn secondary"
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Get Started clicked', {
source: 'Service Metrics',
});
@@ -74,7 +73,7 @@ const EmptyState = memo(
activeLicenseV3 &&
activeLicenseV3.platform === LicensePlatform.CLOUD
) {
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
@@ -117,7 +116,7 @@ const ServicesListTable = memo(
onRowClick,
}: {
services: ServicesList[];
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
onRowClick: (record: ServicesList) => void;
}): JSX.Element => (
<div className="services-list-container home-data-item-container metrics-services-list">
<div className="services-list">
@@ -126,8 +125,8 @@ const ServicesListTable = memo(
dataSource={services}
pagination={false}
className="services-table"
onRow={(record): { onClick: (event: React.MouseEvent) => void } => ({
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
onRow={(record): { onClick: () => void } => ({
onClick: (): void => onRowClick(record),
})}
/>
</div>
@@ -285,19 +284,11 @@ function ServiceMetrics({
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
const handleRowClick = useCallback(
(record: ServicesList, event: React.MouseEvent) => {
(record: ServicesList) => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
if (event && isCtrlOrMMetaKey(event)) {
window.open(
`${ROUTES.APPLICATION}/${record.serviceName}`,
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
}
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
},
[safeNavigate],
);

View File

@@ -3,6 +3,7 @@ import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { useQueryService } from 'hooks/useQueryService';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { ArrowRight, ArrowUpRight } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
@@ -14,8 +15,6 @@ import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { genericNavigate } from 'utils/genericNavigate';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -119,7 +118,7 @@ export default function ServiceTraces({
<Button
type="default"
className="periscope-btn secondary"
onClick={(event: React.MouseEvent): void => {
onClick={(): void => {
logEvent('Homepage: Get Started clicked', {
source: 'Service Traces',
});
@@ -128,7 +127,7 @@ export default function ServiceTraces({
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
@@ -173,21 +172,13 @@ export default function ServiceTraces({
dataSource={top5Services}
pagination={false}
className="services-table"
onRow={(record): { onClick: (event: React.MouseEvent) => void } => ({
onClick: (event: React.MouseEvent): void => {
onRow={(record): { onClick: () => void } => ({
onClick: (): void => {
logEvent('Homepage: Service clicked', {
serviceName: record.serviceName,
});
if (event && isCtrlOrMMetaKey(event)) {
window.open(
`${ROUTES.APPLICATION}/${record.serviceName}`,
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
}
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
},
})}
/>

View File

@@ -5,10 +5,10 @@ import { Button, Divider, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { genericNavigate } from 'utils/genericNavigate';
import AlertInfoCard from './AlertInfoCard';
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
@@ -36,9 +36,9 @@ export function AlertsEmptyState(): JSX.Element {
const [loading, setLoading] = useState(false);
const onClickNewAlertHandler = useCallback((event: React.MouseEvent): void => {
const onClickNewAlertHandler = useCallback(() => {
setLoading(false);
genericNavigate(ROUTES.ALERTS_NEW, event);
history.push(ROUTES.ALERTS_NEW);
}, []);
return (
@@ -70,9 +70,7 @@ export function AlertsEmptyState(): JSX.Element {
<div className="action-container">
<Button
className="add-alert-btn"
onClick={(event: React.MouseEvent): void =>
onClickNewAlertHandler(event)
}
onClick={onClickNewAlertHandler}
icon={<PlusOutlined />}
disabled={!addNewAlert}
loading={loading}

View File

@@ -31,7 +31,6 @@ import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { GettableAlert } from 'types/api/alerts/get';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import DeleteAlert from './DeleteAlert';
import { ColumnButton, SearchContainer } from './styles';
@@ -267,7 +266,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
e.stopPropagation();
e.preventDefault();
onEditHandler(record, isCtrlOrMMetaKey(e));
onEditHandler(record, e.metaKey || e.ctrlKey);
};
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;

View File

@@ -118,7 +118,7 @@ const templatesList: DashboardTemplate[] = [
interface DashboardTemplatesModalProps {
showNewDashboardTemplatesModal: boolean;
onCreateNewDashboard: (event: React.MouseEvent) => void;
onCreateNewDashboard: () => void;
onCancel: () => void;
}
@@ -204,9 +204,7 @@ export default function DashboardTemplatesModal({
type="primary"
className="periscope-btn primary"
icon={<Plus size={14} />}
onClick={(event: React.MouseEvent): void =>
onCreateNewDashboard(event)
}
onClick={onCreateNewDashboard}
>
New dashboard
</Button>

View File

@@ -86,7 +86,6 @@ import {
Widgets,
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
@@ -285,48 +284,35 @@ function DashboardsList(): JSX.Element {
refetchDashboardList,
})) || [];
const onNewDashboardHandler = useCallback(
async (event: React.MouseEvent): Promise<void> => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V5,
});
const onNewDashboardHandler = useCallback(async () => {
try {
logEvent('Dashboard List: Create dashboard clicked', {});
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V5,
});
if (event && isCtrlOrMMetaKey(event)) {
window.open(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
'_blank',
'noopener,noreferrer',
);
} else {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
}
} catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
},
[newDashboardState, safeNavigate, showErrorModal, t],
);
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
} catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, safeNavigate, showErrorModal, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
@@ -428,8 +414,8 @@ function DashboardsList(): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (isCtrlOrMMetaKey(event)) {
window.open(getLink(), '_blank', 'noopener,noreferrer');
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}
@@ -655,8 +641,8 @@ function DashboardsList(): JSX.Element {
label: (
<div
className="create-dashboard-menu-item"
onClick={(event: React.MouseEvent): void => {
onNewDashboardHandler(event);
onClick={(): void => {
onNewDashboardHandler();
}}
>
<LayoutGrid size={14} /> Create dashboard
@@ -943,9 +929,7 @@ function DashboardsList(): JSX.Element {
<DashboardTemplatesModal
showNewDashboardTemplatesModal={showNewDashboardTemplatesModal}
onCreateNewDashboard={(event: React.MouseEvent): Promise<void> =>
onNewDashboardHandler(event)
}
onCreateNewDashboard={onNewDashboardHandler}
onCancel={(): void => {
setShowNewDashboardTemplatesModal(false);
}}

View File

@@ -1,6 +1,6 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import { genericNavigate } from 'utils/genericNavigate';
import history from 'lib/history';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
@@ -11,7 +11,11 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${DashboardId}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
genericNavigate(getLink(), event);
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
history.push(getLink());
}
};
return (

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -180,17 +179,14 @@ function DBCall(): JSX.Element {
type="default"
size="small"
id="database_call_rps_button"
onClick={(event): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
@@ -219,17 +215,14 @@ function DBCall(): JSX.Element {
type="default"
size="small"
id="database_call_avg_duration_button"
onClick={(event): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-identical-functions */
import { Col } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -245,28 +244,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_error_percentage_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address || '',
isError: true,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address || '',
isError: true,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_error_percentage">
<GraphContainer>
@@ -293,28 +286,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_duration_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_duration">
@@ -344,28 +331,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_rps_by_address_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_rps_by_address">
<GraphContainer>
@@ -392,28 +373,22 @@ function External(): JSX.Element {
<Col span={12}>
<GraphControlsPanel
id="external_call_duration_by_address_button"
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
event,
})
}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_duration_by_address">

View File

@@ -1,4 +1,3 @@
/* eslint-disable sonarjs/no-identical-functions */
import logEvent from 'api/common/logEvent';
import getTopLevelOperations, {
ServiceDataProps,
@@ -32,7 +31,6 @@ 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 { genericNavigate } from 'utils/genericNavigate';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -230,16 +228,14 @@ function Application(): JSX.Element {
* @param timestamp - The timestamp in seconds
* @param apmToTraceQuery - query object
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
* @param event - Click event to handle opening in new tab
* @returns A callback function that handles the navigation when executed
*/
const onErrorTrackHandler = useCallback(
(
timestamp: number,
apmToTraceQuery: Query,
event: React.MouseEvent,
isViewLogsClicked?: boolean,
): void => {
): (() => void) => (): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - stepInterval);
@@ -263,7 +259,7 @@ function Application(): JSX.Element {
queryString,
);
genericNavigate(newPath, event);
history.push(newPath);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[stepInterval],
@@ -323,17 +319,14 @@ function Application(): JSX.Element {
type="default"
size="small"
id="Rate_button"
onClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
@@ -356,17 +349,14 @@ function Application(): JSX.Element {
type="default"
size="small"
id="ApDex_button"
onClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
@@ -380,12 +370,15 @@ function Application(): JSX.Element {
<ColErrorContainer>
<GraphControlsPanel
id="Error_button"
onViewLogsClick={(event: React.MouseEvent): void =>
onErrorTrackHandler(selectedTimeStamp, logErrorQuery, event, true)
}
onViewTracesClick={(event: React.MouseEvent): void =>
onErrorTrackHandler(selectedTimeStamp, errorTrackQuery, event)
}
onViewLogsClick={onErrorTrackHandler(
selectedTimeStamp,
logErrorQuery,
true,
)}
onViewTracesClick={onErrorTrackHandler(
selectedTimeStamp,
errorTrackQuery,
)}
/>
<TopLevelOperation

View File

@@ -6,9 +6,9 @@ import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
interface GraphControlsPanelProps {
id: string;
onViewLogsClick?: (event: React.MouseEvent) => void;
onViewTracesClick: (event: React.MouseEvent) => void;
onViewAPIMonitoringClick?: (event: React.MouseEvent) => void;
onViewLogsClick?: () => void;
onViewTracesClick: () => void;
onViewAPIMonitoringClick?: () => void;
}
function GraphControlsPanel({
@@ -23,7 +23,7 @@ function GraphControlsPanel({
type="link"
icon={<DraftingCompass size={14} />}
size="small"
onClick={(event: React.MouseEvent): void => onViewTracesClick(event)}
onClick={onViewTracesClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View traces
@@ -33,7 +33,7 @@ function GraphControlsPanel({
type="link"
icon={<ScrollText size={14} />}
size="small"
onClick={(event: React.MouseEvent): void => onViewLogsClick(event)}
onClick={onViewLogsClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View logs
@@ -44,9 +44,7 @@ function GraphControlsPanel({
type="link"
icon={<Binoculars size={14} />}
size="small"
onClick={(event: React.MouseEvent): void =>
onViewAPIMonitoringClick(event)
}
onClick={onViewAPIMonitoringClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View External APIs

View File

@@ -103,29 +103,23 @@ function ServiceOverview({
<>
<GraphControlsPanel
id="Service_button"
onViewLogsClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
safeNavigate,
event,
})
}
onViewTracesClick={(event: React.MouseEvent): void =>
onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
event,
})
}
onViewLogsClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
safeNavigate,
})}
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
/>
<Card data-testid="service_latency">
<GraphContainer>

View File

@@ -3,7 +3,6 @@ import { navigateToTrace } from 'container/MetricsApplication/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { v4 as uuid } from 'uuid';
import { useGetAPMToTracesQueries } from '../../util';
@@ -51,7 +50,7 @@ function ColumnWithLink({
return (
<Tooltip placement="topLeft" title={text}>
<Typography.Link
onClick={(e): void => handleOnClick(text, isCtrlOrMMetaKey(e))}
onClick={(e): void => handleOnClick(text, e.metaKey || e.ctrlKey)}
>
{text}
</Typography.Link>

View File

@@ -22,7 +22,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -44,7 +43,6 @@ interface OnViewTracePopupClickProps {
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
event: React.MouseEvent;
}
interface OnViewAPIMonitoringPopupClickProps {
@@ -55,7 +53,6 @@ interface OnViewAPIMonitoringPopupClickProps {
isError: boolean;
safeNavigate: (url: string) => void;
event: React.MouseEvent;
}
export function generateExplorerPath(
@@ -86,7 +83,7 @@ export function generateExplorerPath(
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
* @param stepInterval - Time interval in seconds
* @param safeNavigate - Navigation function
* @param event - Click event to handle opening in new tab
*/
export function onViewTracePopupClick({
selectedTraceTags,
@@ -96,34 +93,33 @@ export function onViewTracePopupClick({
isViewLogsClicked,
stepInterval,
safeNavigate,
event,
}: OnViewTracePopupClickProps): void {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery));
const JSONCompositeQuery = encodeURIComponent(
JSON.stringify(apmToTraceQuery),
);
const newPath = generateExplorerPath(
isViewLogsClicked,
urlParams,
servicename,
selectedTraceTags,
JSONCompositeQuery,
queryString,
);
const newPath = generateExplorerPath(
isViewLogsClicked,
urlParams,
servicename,
selectedTraceTags,
JSONCompositeQuery,
queryString,
);
if (event && isCtrlOrMMetaKey(event)) {
window.open(newPath, '_blank', 'noopener,noreferrer');
} else {
safeNavigate(newPath);
}
};
}
const generateAPIMonitoringPath = (
@@ -153,52 +149,49 @@ export function onViewAPIMonitoringPopupClick({
isError,
stepInterval,
safeNavigate,
event,
}: OnViewAPIMonitoringPopupClickProps): void {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
items: [
...(isError
? [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
id: 'hasError--bool--tag--true',
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
return (): void => {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
items: [
...(isError
? [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
id: 'hasError--bool--tag--true',
},
op: 'in',
value: ['true'],
},
op: 'in',
value: ['true'],
},
]
: []),
{
id: uuid().slice(0, 8),
key: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
]
: []),
{
id: uuid().slice(0, 8),
key: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
},
op: '=',
value: servicename,
},
op: '=',
value: servicename,
},
],
op: 'AND',
};
const newPath = generateAPIMonitoringPath(
domainName,
startTime,
endTime,
filters,
);
],
op: 'AND',
};
const newPath = generateAPIMonitoringPath(
domainName,
startTime,
endTime,
filters,
);
if (event && isCtrlOrMMetaKey(event)) {
window.open(newPath, '_blank', 'noopener,noreferrer');
} else {
safeNavigate(newPath);
}
};
}
export function useGraphClickHandler(

View File

@@ -17,7 +17,6 @@ import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { v4 as uuid } from 'uuid';
import { IServiceName } from './Tabs/types';
@@ -116,7 +115,7 @@ function TopOperationsTable({
e.stopPropagation();
e.preventDefault();
if (isCtrlOrMMetaKey(e)) {
if (e.metaKey || e.ctrlKey) {
handleOnClick(text, true); // open in new tab
} else {
handleOnClick(text, false); // open in current tab

View File

@@ -1,14 +1,13 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './NoLogs.styles.scss';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import DOCLINKS from 'utils/docLinks';
import { genericNavigate } from 'utils/genericNavigate';
export default function NoLogs({
dataSource,
@@ -17,8 +16,6 @@ export default function NoLogs({
}): JSX.Element {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const REL_NOOPENER_NOREFERRER = 'noopener,noreferrer';
const handleLinkClick = (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
): void => {
@@ -41,25 +38,13 @@ export default function NoLogs({
} else {
link = ROUTES.GET_STARTED_LOGS_MANAGEMENT;
}
genericNavigate(link, e);
history.push(link);
} else if (dataSource === 'traces') {
window.open(
DOCLINKS.TRACES_EXPLORER_EMPTY_STATE,
'_blank',
REL_NOOPENER_NOREFERRER,
);
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
} else if (dataSource === DataSource.METRICS) {
window.open(
DOCLINKS.METRICS_EXPLORER_EMPTY_STATE,
'_blank',
REL_NOOPENER_NOREFERRER,
);
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
} else {
window.open(
`${DOCLINKS.USER_GUIDE}${dataSource}/`,
'_blank',
REL_NOOPENER_NOREFERRER,
);
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
}
};
return (
@@ -74,12 +59,7 @@ export default function NoLogs({
</span>
</Typography>
<Typography.Link
className="send-logs-link"
onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): void =>
handleLinkClick(e)
}
>
<Typography.Link className="send-logs-link" onClick={handleLinkClick}>
Sending {dataSource} to SigNoz <ArrowUpRight size={16} />
</Typography.Link>
</div>

View File

@@ -48,7 +48,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -226,7 +225,7 @@ function QueryBuilderSearch({
if (
!disableNavigationShortcuts &&
isCtrlOrMMetaKey(event) &&
(event.ctrlKey || event.metaKey) &&
event.key === 'Enter'
) {
event.preventDefault();
@@ -237,7 +236,7 @@ function QueryBuilderSearch({
if (
!disableNavigationShortcuts &&
isCtrlOrMMetaKey(event) &&
(event.ctrlKey || event.metaKey) &&
event.key === '/'
) {
event.preventDefault();

View File

@@ -52,7 +52,6 @@ import {
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -457,12 +456,12 @@ function QueryBuilderSearchV2(
setTags((prev) => prev.slice(0, -1));
}
if (isCtrlOrMMetaKey(event) && event.key === '/') {
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
event.preventDefault();
event.stopPropagation();
setShowAllFilters((prev) => !prev);
}
if (isCtrlOrMMetaKey(event) && event.key === 'Enter') {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
handleRunQuery();

View File

@@ -67,8 +67,6 @@ import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { genericNavigate } from 'utils/genericNavigate';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config';
@@ -294,6 +292,8 @@ 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 [
@@ -411,7 +411,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const openInNewTab = (path: string): void => {
window.open(path, '_blank', 'noopener,noreferrer');
window.open(path, '_blank');
};
const onClickGetStarted = (event: MouseEvent): void => {
@@ -424,7 +424,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
? ROUTES.GET_STARTED_WITH_CLOUD
: ROUTES.GET_STARTED;
if (isCtrlOrMMetaKey(event)) {
if (isCtrlMetaKey(event)) {
openInNewTab(onboaringRoute);
} else {
history.push(onboaringRoute);
@@ -439,7 +439,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const queryString = getQueryString(availableParams || [], params);
if (pathname !== key) {
if (event && isCtrlOrMMetaKey(event)) {
if (event && isCtrlMetaKey(event)) {
openInNewTab(`${key}?${queryString.join('&')}`);
} else {
history.push(`${key}?${queryString.join('&')}`, {
@@ -634,7 +634,11 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
if (item.key === ROUTES.SETTINGS) {
genericNavigate(settingsRoute, event);
if (isCtrlMetaKey(event)) {
openInNewTab(settingsRoute);
} else {
history.push(settingsRoute);
}
} else if (item.key === 'quick-search') {
openCmdK();
} else if (item) {
@@ -805,7 +809,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
);
if (item && !('type' in item) && item.isExternal && item.url) {
window.open(item.url, '_blank', 'noopener,noreferrer');
window.open(item.url, '_blank');
}
if (item && !('type' in item)) {

View File

@@ -10,7 +10,9 @@ import {
import { formUrlParams } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import history from 'lib/history';
import omit from 'lodash-es/omit';
import { HTMLAttributes } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { updateURL } from 'store/actions/trace/util';
@@ -23,7 +25,6 @@ import {
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { genericNavigate } from 'utils/genericNavigate';
import { v4 } from 'uuid';
dayjs.extend(duration);
@@ -202,13 +203,15 @@ function TraceTable(): JSX.Element {
style={{
cursor: 'pointer',
}}
onRow={(
record: TableType,
): { onClick: (event: React.MouseEvent) => void } => ({
onClick: (event: React.MouseEvent): void => {
onRow={(record: TableType): HTMLAttributes<TableType> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
genericNavigate(getLink(record), event);
if (event.metaKey || event.ctrlKey) {
window.open(getLink(record), '_blank');
} else {
history.push(getLink(record));
}
},
})}
pagination={{

View File

@@ -84,8 +84,8 @@ function TracesTableComponent({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
if (event.ctrlKey || event.metaKey) {
window.open(getTraceLink(record), '_blank', 'noopener,noreferrer');
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
} else {
history.push(getTraceLink(record));
}

View File

@@ -27,7 +27,6 @@ import {
within,
} from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import * as genericNavigate from 'utils/genericNavigate';
import TracesExplorer from '..';
import { Filter } from '../Filter/Filter';
@@ -773,12 +772,6 @@ describe('TracesExplorer - ', () => {
});
it('create an alert btn assert', async () => {
const historyPush = jest.fn();
jest.spyOn(genericNavigate, 'genericNavigate').mockImplementation((link) => {
historyPush(link);
});
const { getByText } = renderWithTracesExplorerRouter(<TracesExplorer />, [
'/traces-explorer/?panelType=list&selectedExplorerView=list',
]);

View File

@@ -1,18 +0,0 @@
import history from 'lib/history';
import { KeyboardEvent, MouseEvent } from 'react';
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
export const genericNavigate = (
link: string,
event?:
| MouseEvent
| KeyboardEvent
| globalThis.MouseEvent
| globalThis.KeyboardEvent,
): void => {
if (event && isCtrlOrMMetaKey(event)) {
window.open(link, '_blank', 'noopener,noreferrer');
} else {
history.push(link);
}
};

View File

@@ -1,9 +0,0 @@
import { KeyboardEvent, MouseEvent } from 'react';
export const isCtrlOrMMetaKey = (
event:
| MouseEvent
| KeyboardEvent
| globalThis.MouseEvent
| globalThis.KeyboardEvent,
): boolean => event.metaKey || event.ctrlKey;

View File

@@ -315,22 +315,5 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(provider.authZ.OpenAccess(provider.userHandler.ForgotPassword), handler.OpenAPIDef{
ID: "ForgotPassword",
Tags: []string{"users"},
Summary: "Forgot password",
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
Request: new(types.PostableForgotPassword),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: []handler.OpenAPISecurityScheme{},
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -1,43 +0,0 @@
package user
import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
)
type Config struct {
Password PasswordConfig `mapstructure:"password"`
}
type PasswordConfig struct {
Reset ResetConfig `mapstructure:"reset"`
}
type ResetConfig struct {
AllowSelf bool `mapstructure:"allow_self"`
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("user"), newConfig)
}
func newConfig() factory.Config {
return &Config{
Password: PasswordConfig{
Reset: ResetConfig{
AllowSelf: false,
MaxTokenLifetime: 6 * time.Hour,
},
},
}
}
func (c Config) Validate() error {
if c.Password.Reset.MaxTokenLifetime <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
}
return nil
}

View File

@@ -332,25 +332,6 @@ func (handler *handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
req := new(types.PostableForgotPassword)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(w, err)
return
}
err := h.module.ForgotPassword(ctx, req.OrgID, req.Email, req.FrontendBaseURL)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

View File

@@ -12,13 +12,11 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
root "github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/tokenizer"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
@@ -30,11 +28,10 @@ type Module struct {
settings factory.ScopedProviderSettings
orgSetter organization.Setter
analytics analytics.Analytics
config user.Config
}
// This module is a WIP, don't take inspiration from this.
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics, config user.Config) root.Module {
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics) root.Module {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
return &Module{
store: store,
@@ -43,7 +40,6 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
settings: settings,
orgSetter: orgSetter,
analytics: analytics,
config: config,
}
}
@@ -306,91 +302,33 @@ func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID
}
}
// check if a token already exists for this password id
existingResetPasswordToken, err := module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil, err // return the error if it is not a not found error
resetPasswordToken, err := types.NewResetPasswordToken(password.ID)
if err != nil {
return nil, err
}
// return the existing token if it is not expired
if existingResetPasswordToken != nil && !existingResetPasswordToken.IsExpired() {
return existingResetPasswordToken, nil // return the existing token if it is not expired
}
err = module.store.CreateResetPasswordToken(ctx, resetPasswordToken)
if err != nil {
if !errors.Ast(err, errors.TypeAlreadyExists) {
return nil, err
}
// delete the existing token entry
if existingResetPasswordToken != nil {
if err := module.store.DeleteResetPasswordTokenByPasswordID(ctx, password.ID); err != nil {
// if the token already exists, we return the existing token
resetPasswordToken, err = module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID)
if err != nil {
return nil, err
}
}
// create a new token
resetPasswordToken, err := types.NewResetPasswordToken(password.ID, time.Now().Add(module.config.Password.Reset.MaxTokenLifetime))
if err != nil {
return nil, err
}
// create a new token
err = module.store.CreateResetPasswordToken(ctx, resetPasswordToken)
if err != nil {
return nil, err
}
return resetPasswordToken, nil
}
func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error {
if !module.config.Password.Reset.AllowSelf {
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "users are not allowed to reset their password themselves, please contact an admin to reset your password")
}
user, err := module.store.GetUserByEmailAndOrgID(ctx, email, orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil // for security reasons
}
return err
}
token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID)
if err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", "error", err)
return err
}
resetLink := fmt.Sprintf("%s/password-reset?token=%s", frontendBaseURL, token.Token)
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
if err := module.emailing.SendHTML(
ctx,
user.Email.String(),
"Reset your SigNoz password",
emailtypes.TemplateNameResetPassword,
map[string]any{
"Name": user.DisplayName,
"Link": resetLink,
"Expiry": humanizedTokenLifetime,
},
); err != nil {
module.settings.Logger().ErrorContext(ctx, "failed to send reset password email", "error", err)
return nil
}
return nil
}
func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error {
resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token)
if err != nil {
return err
}
if resetPasswordToken.IsExpired() {
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "reset password token has expired")
}
password, err := module.store.GetPassword(ctx, resetPasswordToken.PasswordID)
if err != nil {
return err

View File

@@ -391,18 +391,6 @@ func (store *store) GetResetPasswordTokenByPasswordID(ctx context.Context, passw
return resetPasswordToken, nil
}
func (store *store) DeleteResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) error {
_, err := store.sqlstore.BunDB().NewDelete().
Model(&types.ResetPasswordToken{}).
Where("password_id = ?", passwordID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete reset password token")
}
return nil
}
func (store *store) GetResetPasswordToken(ctx context.Context, token string) (*types.ResetPasswordToken, error) {
resetPasswordRequest := new(types.ResetPasswordToken)

View File

@@ -30,9 +30,6 @@ type Module interface {
// Updates password of user to the new password. It also deletes all reset password tokens for the user.
UpdatePassword(ctx context.Context, userID valuer.UUID, oldPassword string, password string) error
// Initiate forgot password flow for a user
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error)
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
@@ -95,7 +92,6 @@ type Handler interface {
GetResetPasswordToken(http.ResponseWriter, *http.Request)
ResetPassword(http.ResponseWriter, *http.Request)
ChangePassword(http.ResponseWriter, *http.Request)
ForgotPassword(http.ResponseWriter, *http.Request)
// API KEY
CreateAPIKey(http.ResponseWriter, *http.Request)

View File

@@ -22,7 +22,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/ruler"
@@ -110,9 +109,6 @@ type Config struct {
// Flagger config
Flagger flagger.Config `mapstructure:"flagger"`
// User config
User user.Config `mapstructure:"user"`
}
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
@@ -175,7 +171,6 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
tokenizer.NewConfigFactory(),
metricsexplorer.NewConfigFactory(),
flagger.NewConfigFactory(),
user.NewConfigFactory(),
}
conf, err := config.New(ctx, resolverConfig, configFactories)

View File

@@ -88,7 +88,7 @@ func NewModules(
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics, config.User)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)

View File

@@ -161,7 +161,6 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewUpdateUserPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewUpdateOrgPreferenceFactory(sqlstore, sqlschema),
sqlmigration.NewRenameOrgDomainsFactory(sqlstore, sqlschema),
sqlmigration.NewAddResetPasswordTokenExpiryFactory(sqlstore, sqlschema),
)
}

View File

@@ -1,83 +0,0 @@
package sqlmigration
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addResetPasswordTokenExpiry struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddResetPasswordTokenExpiryFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_reset_password_token_expiry"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newAddResetPasswordTokenExpiry(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newAddResetPasswordTokenExpiry(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &addResetPasswordTokenExpiry{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *addResetPasswordTokenExpiry) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addResetPasswordTokenExpiry) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
// get the reset_password_token table
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("reset_password_token"))
if err != nil {
return err
}
// add a new column `expires_at`
column := &sqlschema.Column{
Name: sqlschema.ColumnName("expires_at"),
DataType: sqlschema.DataTypeTimestamp,
Nullable: true,
}
// for existing rows set
defaultValueForExistingRows := time.Now()
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, defaultValueForExistingRows)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addResetPasswordTokenExpiry) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -12,13 +12,12 @@ import (
var (
// Templates is a list of all the templates that are supported by the emailing service.
// This list should be updated whenever a new template is added.
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole, TemplateNameResetPassword}
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole}
)
var (
TemplateNameInvitationEmail = TemplateName{valuer.NewString("invitation_email")}
TemplateNameUpdateRole = TemplateName{valuer.NewString("update_role")}
TemplateNameResetPassword = TemplateName{valuer.NewString("reset_password_email")}
)
type TemplateName struct{ valuer.String }
@@ -29,8 +28,6 @@ func NewTemplateName(name string) (TemplateName, error) {
return TemplateNameInvitationEmail, nil
case TemplateNameUpdateRole.StringValue():
return TemplateNameUpdateRole, nil
case TemplateNameResetPassword.StringValue():
return TemplateNameResetPassword, nil
default:
return TemplateName{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid template name: %s", name)
}

View File

@@ -35,19 +35,12 @@ type ChangePasswordRequest struct {
NewPassword string `json:"newPassword"`
}
type PostableForgotPassword struct {
OrgID valuer.UUID `json:"orgId"`
Email valuer.Email `json:"email"`
FrontendBaseURL string `json:"frontendBaseURL"`
}
type ResetPasswordToken struct {
bun.BaseModel `bun:"table:reset_password_token"`
Identifiable
Token string `bun:"token,type:text,notnull" json:"token"`
PasswordID valuer.UUID `bun:"password_id,type:text,notnull,unique" json:"passwordId"`
ExpiresAt time.Time `bun:"expires_at,type:timestamptz,nullzero" json:"expiresAt"`
}
type FactorPassword struct {
@@ -143,14 +136,13 @@ func NewHashedPassword(password string) (string, error) {
return string(hashedPassword), nil
}
func NewResetPasswordToken(passwordID valuer.UUID, expiresAt time.Time) (*ResetPasswordToken, error) {
func NewResetPasswordToken(passwordID valuer.UUID) (*ResetPasswordToken, error) {
return &ResetPasswordToken{
Identifiable: Identifiable{
ID: valuer.GenerateUUID(),
},
Token: valuer.GenerateUUID().String(),
PasswordID: passwordID,
ExpiresAt: expiresAt,
}, nil
}
@@ -216,7 +208,3 @@ func (f *FactorPassword) Equals(password string) bool {
func comparePassword(hashedPassword string, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
}
func (r *ResetPasswordToken) IsExpired() bool {
return r.ExpiresAt.Before(time.Now())
}

View File

@@ -553,18 +553,6 @@ func (f Function) Copy() Function {
return c
}
// Validate validates the name and args for the function
func (f Function) Validate() error {
if err := f.Name.Validate(); err != nil {
return err
}
// Validate args for function
if err := f.ValidateArgs(); err != nil {
return err
}
return nil
}
type LimitBy struct {
// keys to limit by
Keys []string `json:"keys"`

View File

@@ -73,43 +73,6 @@ func (f *QueryBuilderFormula) UnmarshalJSON(data []byte) error {
return nil
}
// Validate checks if the QueryBuilderFormula fields are valid
func (f QueryBuilderFormula) Validate() error {
// Validate name is not blank
if strings.TrimSpace(f.Name) == "" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"formula name cannot be blank",
)
}
// Validate expression is not blank
if strings.TrimSpace(f.Expression) == "" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"formula expression cannot be blank",
)
}
// Validate functions if present
for i, fn := range f.Functions {
if err := fn.Validate(); err != nil {
fnId := fmt.Sprintf("function #%d", i+1)
if f.Name != "" {
fnId = fmt.Sprintf("function #%d in formula '%s'", i+1, f.Name)
}
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid %s: %s",
fnId,
err.Error(),
)
}
}
return nil
}
// small container to store the query name and index or alias reference
// for a variable in the formula expression
// read below for more details on aggregation references

View File

@@ -1,13 +1,10 @@
package querybuildertypesv5
import (
"fmt"
"math"
"slices"
"strconv"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -36,46 +33,6 @@ var (
FunctionNameFillZero = FunctionName{valuer.NewString("fillZero")}
)
// Validate checks if the FunctionName is valid and one of the known types
func (fn FunctionName) Validate() error {
validFunctions := []FunctionName{
FunctionNameCutOffMin,
FunctionNameCutOffMax,
FunctionNameClampMin,
FunctionNameClampMax,
FunctionNameAbsolute,
FunctionNameRunningDiff,
FunctionNameLog2,
FunctionNameLog10,
FunctionNameCumulativeSum,
FunctionNameEWMA3,
FunctionNameEWMA5,
FunctionNameEWMA7,
FunctionNameMedian3,
FunctionNameMedian5,
FunctionNameMedian7,
FunctionNameTimeShift,
FunctionNameAnomaly,
FunctionNameFillZero,
}
if slices.Contains(validFunctions, fn) {
return nil
}
// Format valid functions as comma-separated string
var validFunctionNames []string
for _, fn := range validFunctions {
validFunctionNames = append(validFunctionNames, fn.StringValue())
}
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid function name: %s",
fn.StringValue(),
).WithAdditional(fmt.Sprintf("valid functions are: %s", strings.Join(validFunctionNames, ", ")))
}
// ApplyFunction applies the given function to the result data
func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
// Extract the function name and arguments
@@ -155,61 +112,6 @@ func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
return result
}
// ValidateArgs validates the arguments for the given function
func (fn Function) ValidateArgs() error {
// Extract the function name and arguments
name := fn.Name
args := fn.Args
switch name {
case FunctionNameCutOffMin, FunctionNameCutOffMax, FunctionNameClampMin, FunctionNameClampMax:
if len(args) == 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"threshold value is required for function %s",
name.StringValue(),
)
}
_, err := parseFloat64Arg(args[0].Value)
if err != nil {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"threshold value must be a floating value for function %s",
name.StringValue(),
)
}
case FunctionNameEWMA3, FunctionNameEWMA5, FunctionNameEWMA7:
if len(args) == 0 {
return nil // alpha is optional for EWMA functions
}
_, err := parseFloat64Arg(args[0].Value)
if err != nil {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"alpha value must be a floating value for function %s",
name.StringValue(),
)
}
case FunctionNameTimeShift:
if len(args) == 0 {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"time shift value is required for function %s",
name.StringValue(),
)
}
_, err := parseFloat64Arg(args[0].Value)
if err != nil {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"time shift value must be a floating value for function %s",
name.StringValue(),
)
}
}
return nil
}
// parseFloat64Arg parses an argument to float64
func parseFloat64Arg(value any) (float64, error) {
switch v := value.(type) {

View File

@@ -65,6 +65,46 @@ const (
MaxQueryLimit = 10000
)
// ValidateFunctionName checks if the function name is valid
func ValidateFunctionName(name FunctionName) error {
validFunctions := []FunctionName{
FunctionNameCutOffMin,
FunctionNameCutOffMax,
FunctionNameClampMin,
FunctionNameClampMax,
FunctionNameAbsolute,
FunctionNameRunningDiff,
FunctionNameLog2,
FunctionNameLog10,
FunctionNameCumulativeSum,
FunctionNameEWMA3,
FunctionNameEWMA5,
FunctionNameEWMA7,
FunctionNameMedian3,
FunctionNameMedian5,
FunctionNameMedian7,
FunctionNameTimeShift,
FunctionNameAnomaly,
FunctionNameFillZero,
}
if slices.Contains(validFunctions, name) {
return nil
}
// Format valid functions as comma-separated string
var validFunctionNames []string
for _, fn := range validFunctions {
validFunctionNames = append(validFunctionNames, fn.StringValue())
}
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"invalid function name: %s",
name.StringValue(),
).WithAdditional(fmt.Sprintf("valid functions are: %s", strings.Join(validFunctionNames, ", ")))
}
// Validate performs preliminary validation on QueryBuilderQuery
func (q *QueryBuilderQuery[T]) Validate(requestType RequestType) error {
// Validate signal
@@ -271,7 +311,7 @@ func (q *QueryBuilderQuery[T]) validateLimitAndPagination() error {
func (q *QueryBuilderQuery[T]) validateFunctions() error {
for i, fn := range q.Functions {
if err := fn.Validate(); err != nil {
if err := ValidateFunctionName(fn.Name); err != nil {
fnId := fmt.Sprintf("function #%d", i+1)
if q.Name != "" {
fnId = fmt.Sprintf("function #%d in query '%s'", i+1, q.Name)

View File

@@ -143,7 +143,6 @@ type UserStore interface {
GetPasswordByUserID(ctx context.Context, userID valuer.UUID) (*FactorPassword, error)
GetResetPasswordToken(ctx context.Context, token string) (*ResetPasswordToken, error)
GetResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) (*ResetPasswordToken, error)
DeleteResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) error
UpdatePassword(ctx context.Context, password *FactorPassword) error
// API KEY

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<p>Hello {{.Name}},</p>
<p>You requested a password reset for your SigNoz account.</p>
<p>Click the link below to reset your password:</p>
<a href="{{.Link}}">Reset Password</a>
<p>This link will expire in {{.Expiry}}.</p>
<p>If you didn't request this, please ignore this email. Your password will remain unchanged.</p>
<br>
<p>Best regards,<br>The SigNoz Team</p>
</body>
</html>

View File

@@ -67,8 +67,6 @@ def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
"SIGNOZ_GATEWAY_URL": gateway.container_configs["8080"].base(),
"SIGNOZ_TOKENIZER_JWT_SECRET": "secret",
"SIGNOZ_GLOBAL_INGESTION__URL": "https://ingest.test.signoz.cloud",
"SIGNOZ_USER_PASSWORD_RESET_ALLOW__SELF": True,
"SIGNOZ_USER_PASSWORD_RESET_MAX__TOKEN__LIFETIME": "6h",
}
| sqlstore.env
| clickhouse.env

View File

@@ -7,8 +7,6 @@ from sqlalchemy import sql
from fixtures import types
from fixtures.logger import setup_logger
from datetime import datetime, timedelta, timezone
logger = setup_logger(__name__)
@@ -242,261 +240,3 @@ def test_reset_password_with_no_password(
token = get_token("admin+password@integration.test", "FINALPASSword123!#[")
assert token is not None
def test_forgot_password_returns_204_for_nonexistent_email(
signoz: types.SigNoz,
) -> None:
"""
Test that forgotPassword returns 204 even for non-existent emails
(for security reasons - doesn't reveal if user exists).
"""
# Get org ID first (needed for the forgot password request)
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/sessions/context"),
params={
"email": "admin@integration.test",
"ref": f"{signoz.self.host_configs['8080'].base()}",
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK
org_id = response.json()["data"]["orgs"][0]["id"]
# Call forgot password with a non-existent email
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/factor_password/forgot"),
json={
"email": "nonexistent@integration.test",
"orgId": org_id,
"frontendBaseURL": signoz.self.host_configs["8080"].base(),
},
timeout=5,
)
# Should return 204 even for non-existent email (security)
assert response.status_code == HTTPStatus.NO_CONTENT
def test_forgot_password_creates_reset_token(
signoz: types.SigNoz, get_token: Callable[[str, str], str]
) -> None:
"""
Test the full forgot password flow:
1. Call forgotPassword endpoint for existing user
2. Verify reset password token is created in database
3. Use the token to reset password
4. Verify user can login with new password
"""
admin_token = get_token("admin@integration.test", "password123Z$")
# Create a user specifically for testing forgot password
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite"),
json={"email": "forgot@integration.test", "role": "EDITOR", "name": "forgotpassword user"},
timeout=2,
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == HTTPStatus.CREATED
# Get the invite token
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/invite"),
timeout=2,
headers={"Authorization": f"Bearer {admin_token}"},
)
invite_response = response.json()["data"]
found_invite = next(
(
invite
for invite in invite_response
if invite["email"] == "forgot@integration.test"
),
None,
)
# Accept the invite to create the user
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
json={
"password": "originalPassword123Z$",
"displayName": "forgotpassword user",
"token": f"{found_invite['token']}",
},
timeout=2,
)
assert response.status_code == HTTPStatus.CREATED
# Get org ID
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/sessions/context"),
params={
"email": "forgot@integration.test",
"ref": f"{signoz.self.host_configs['8080'].base()}",
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK
org_id = response.json()["data"]["orgs"][0]["id"]
# Call forgot password endpoint
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/factor_password/forgot"),
json={
"email": "forgot@integration.test",
"orgId": org_id,
"frontendBaseURL": signoz.self.host_configs["8080"].base(),
},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT
# Verify reset password token was created by querying the database
# First, get the user ID
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/user"),
timeout=2,
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == HTTPStatus.OK
user_response = response.json()["data"]
found_user = next(
(
user
for user in user_response
if user["email"] == "forgot@integration.test"
),
None,
)
assert found_user is not None
reset_token = None
# Query the database directly to get the reset password token
# First get the password_id from factor_password, then get the token
with signoz.sqlstore.conn.connect() as conn:
result = conn.execute(
sql.text("""
SELECT rpt.token
FROM reset_password_token rpt
JOIN factor_password fp ON rpt.password_id = fp.id
WHERE fp.user_id = :user_id
"""),
{"user_id": found_user["id"]},
)
row = result.fetchone()
assert row is not None, "Reset password token should exist after calling forgotPassword"
reset_token = row[0]
assert reset_token is not None
assert reset_token != ""
# Reset password with a valid strong password
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
json={"password": "newSecurePassword123Z$!", "token": reset_token},
timeout=2,
)
assert response.status_code == HTTPStatus.NO_CONTENT
# Verify user can login with the new password
user_token = get_token("forgot@integration.test", "newSecurePassword123Z$!")
assert user_token is not None
# Verify old password no longer works
try:
get_token("forgot@integration.test", "originalPassword123Z$")
assert False, "Old password should not work after reset"
except AssertionError:
pass # Expected - old password should fail
def test_reset_password_with_expired_token(
signoz: types.SigNoz, get_token: Callable[[str, str], str]
) -> None:
"""
Test that resetting password with an expired token fails.
"""
admin_token = get_token("admin@integration.test", "password123Z$")
# Get user ID for the forgot@integration.test user
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/user"),
timeout=2,
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == HTTPStatus.OK
user_response = response.json()["data"]
found_user = next(
(
user
for user in user_response
if user["email"] == "forgot@integration.test"
),
None,
)
assert found_user is not None
# Get org ID
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v2/sessions/context"),
params={
"email": "forgot@integration.test",
"ref": f"{signoz.self.host_configs['8080'].base()}",
},
timeout=5,
)
assert response.status_code == HTTPStatus.OK
org_id = response.json()["data"]["orgs"][0]["id"]
# Call forgot password to generate a new token
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/factor_password/forgot"),
json={
"email": "forgot@integration.test",
"orgId": org_id,
"frontendBaseURL": signoz.self.host_configs["8080"].base(),
},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT
# Query the database to get the token and then expire it
reset_token = None
with signoz.sqlstore.conn.connect() as conn:
# First get the token
result = conn.execute(
sql.text("""
SELECT rpt.token, rpt.id
FROM reset_password_token rpt
JOIN factor_password fp ON rpt.password_id = fp.id
WHERE fp.user_id = :user_id
"""),
{"user_id": found_user["id"]},
)
row = result.fetchone()
assert row is not None, "Reset password token should exist"
reset_token = row[0]
token_id = row[1]
# Now expire the token by setting expires_at to a past time
conn.execute(
sql.text("""
UPDATE reset_password_token
SET expires_at = :expired_time
WHERE id = :token_id
"""),
{
"expired_time": "2020-01-01 00:00:00",
"token_id": token_id,
},
)
conn.commit()
assert reset_token is not None
# Try to use the expired token - should fail with 401 Unauthorized
response = requests.post(
signoz.self.host_configs["8080"].get("/api/v1/resetPassword"),
json={"password": "expiredTokenPassword123Z$!", "token": reset_token},
timeout=2,
)
assert response.status_code == HTTPStatus.UNAUTHORIZED