mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-07 10:22:12 +00:00
Compare commits
44 Commits
refactor/u
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
543fe613a5 | ||
|
|
512e0519ee | ||
|
|
d56ab18691 | ||
|
|
9d95703539 | ||
|
|
7dc54530ce | ||
|
|
e712434e01 | ||
|
|
91ec60b923 | ||
|
|
325974292f | ||
|
|
7c1a531d01 | ||
|
|
5a45532a72 | ||
|
|
e9501d2e0f | ||
|
|
c306e66bcd | ||
|
|
767a0cc28e | ||
|
|
4f9efcc133 | ||
|
|
bf2dd612e0 | ||
|
|
d3f15022a4 | ||
|
|
a5c021e96c | ||
|
|
8b9fcae0cb | ||
|
|
e75abcb108 | ||
|
|
ffa15f9ec1 | ||
|
|
87562f5387 | ||
|
|
11a7512a25 | ||
|
|
f925a15f30 | ||
|
|
a3fe2c2589 | ||
|
|
2719c9b6a7 | ||
|
|
b4282be3ac | ||
|
|
15fe3a7163 | ||
|
|
30b98582b4 | ||
|
|
99b9e27bca | ||
|
|
7e72f501a7 | ||
|
|
97d7b81a73 | ||
|
|
9c5b50ae3a | ||
|
|
fd13c6dee4 | ||
|
|
a894b08255 | ||
|
|
2c6c034e60 | ||
|
|
0a81bf8060 | ||
|
|
b38ffce7f2 | ||
|
|
696d83eeaf | ||
|
|
d98851dcf2 | ||
|
|
a103bb453d | ||
|
|
abae0c7f27 | ||
|
|
0cc1fb0edb | ||
|
|
f2ef42bd7f | ||
|
|
22158a3cee |
85
.github/CODEOWNERS
vendored
85
.github/CODEOWNERS
vendored
@@ -1,10 +1,13 @@
|
||||
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
# Owners are automatically requested for review for PRs that changes code
|
||||
|
||||
# that they own.
|
||||
|
||||
/frontend/ @SigNoz/frontend-maintainers
|
||||
|
||||
# Onboarding
|
||||
|
||||
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
||||
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
||||
|
||||
@@ -12,42 +15,104 @@
|
||||
.github @SigNoz/devops
|
||||
|
||||
# Scaffold Owners
|
||||
/pkg/config/ @therealpandey
|
||||
/pkg/errors/ @therealpandey
|
||||
/pkg/factory/ @therealpandey
|
||||
/pkg/types/ @therealpandey
|
||||
/pkg/valuer/ @therealpandey
|
||||
/cmd/ @therealpandey
|
||||
.golangci.yml @therealpandey
|
||||
|
||||
/pkg/config/ @vikrantgupta25
|
||||
/pkg/errors/ @vikrantgupta25
|
||||
/pkg/factory/ @vikrantgupta25
|
||||
/pkg/types/ @vikrantgupta25
|
||||
/pkg/valuer/ @vikrantgupta25
|
||||
/cmd/ @vikrantgupta25
|
||||
.golangci.yml @vikrantgupta25
|
||||
|
||||
# Zeus Owners
|
||||
|
||||
/pkg/zeus/ @vikrantgupta25
|
||||
/ee/zeus/ @vikrantgupta25
|
||||
/pkg/licensing/ @vikrantgupta25
|
||||
/ee/licensing/ @vikrantgupta25
|
||||
|
||||
# SQL Owners
|
||||
|
||||
/pkg/sqlmigration/ @vikrantgupta25
|
||||
/ee/sqlmigration/ @vikrantgupta25
|
||||
/pkg/sqlschema/ @vikrantgupta25
|
||||
/ee/sqlschema/ @vikrantgupta25
|
||||
|
||||
# Analytics Owners
|
||||
|
||||
/pkg/analytics/ @vikrantgupta25
|
||||
/pkg/statsreporter/ @vikrantgupta25
|
||||
|
||||
# Querier Owners
|
||||
|
||||
/pkg/querier/ @srikanthccv
|
||||
/pkg/variables/ @srikanthccv
|
||||
/pkg/types/querybuildertypes/ @srikanthccv
|
||||
/pkg/types/telemetrytypes/ @srikanthccv
|
||||
/pkg/querybuilder/ @srikanthccv
|
||||
/pkg/telemetrylogs/ @srikanthccv
|
||||
/pkg/telemetrymetadata/ @srikanthccv
|
||||
/pkg/telemetrymetrics/ @srikanthccv
|
||||
/pkg/telemetrytraces/ @srikanthccv
|
||||
|
||||
# AuthN / AuthZ Owners
|
||||
/pkg/authz/ @vikrantgupta25 @therealpandey
|
||||
|
||||
# Metrics
|
||||
|
||||
/pkg/types/metrictypes/ @srikanthccv
|
||||
/pkg/types/metricsexplorertypes/ @srikanthccv
|
||||
/pkg/modules/metricsexplorer/ @srikanthccv
|
||||
/pkg/prometheus/ @srikanthccv
|
||||
|
||||
# APM
|
||||
|
||||
/pkg/types/servicetypes/ @srikanthccv
|
||||
/pkg/types/apdextypes/ @srikanthccv
|
||||
/pkg/modules/apdex/ @srikanthccv
|
||||
/pkg/modules/services/ @srikanthccv
|
||||
|
||||
# Dashboard
|
||||
|
||||
/pkg/types/dashboardtypes/ @srikanthccv
|
||||
/pkg/modules/dashboard/ @srikanthccv
|
||||
|
||||
# Rule/Alertmanager
|
||||
|
||||
/pkg/types/ruletypes/ @srikanthccv
|
||||
/pkg/types/alertmanagertypes @srikanthccv
|
||||
/pkg/alertmanager/ @srikanthccv
|
||||
/pkg/ruler/ @srikanthccv
|
||||
|
||||
# Correlation-adjacent
|
||||
|
||||
/pkg/contextlinks/ @srikanthccv
|
||||
/pkg/types/parsertypes/ @srikanthccv
|
||||
/pkg/queryparser/ @srikanthccv
|
||||
|
||||
# AuthN / AuthZ Owners
|
||||
|
||||
/pkg/authz/ @vikrantgupta25
|
||||
|
||||
# Integration tests
|
||||
/tests/integration/ @therealpandey
|
||||
|
||||
/tests/integration/ @vikrantgupta25
|
||||
|
||||
# Dashboard Owners
|
||||
|
||||
/frontend/src/hooks/dashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard List
|
||||
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Page
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||
|
||||
## Public Dashboard Page
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
96
.github/pull_request_template.md
vendored
96
.github/pull_request_template.md
vendored
@@ -1,86 +1,76 @@
|
||||
## 📄 Summary
|
||||
|
||||
<!-- Describe the purpose of the PR in a few sentences. What does it fix/add/update? -->
|
||||
## Pull Request
|
||||
|
||||
---
|
||||
|
||||
## ✅ Changes
|
||||
|
||||
- [ ] Feature: Brief description
|
||||
- [ ] Bug fix: Brief description
|
||||
### 📄 Summary
|
||||
> Why does this change exist?
|
||||
> What problem does it solve, and why is this the right approach?
|
||||
|
||||
---
|
||||
|
||||
### ✅ Change Type
|
||||
_Select all that apply_
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
> Fill this only if the change affects users, APIs, UI, or documented behavior.
|
||||
Mention as N/A for internal refactors or non-user-visible changes.
|
||||
|
||||
**Deployment Type:** Cloud / OSS / Enterprise
|
||||
|
||||
**Type:** Feature / Bug Fix / Maintenance
|
||||
|
||||
**Description:** Short, user-facing summary of the change
|
||||
- [ ] ✨ Feature
|
||||
- [ ] 🐛 Bug fix
|
||||
- [ ] ♻️ Refactor
|
||||
- [ ] 🛠️ Infra / Tooling
|
||||
- [ ] 🧪 Test-only
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Required: Add Relevant Labels
|
||||
### 🐛 Bug Context
|
||||
> Required if this PR fixes a bug
|
||||
|
||||
> ⚠️ **Manually add appropriate labels in the PR sidebar**
|
||||
Please select one or more labels (as applicable):
|
||||
#### Root Cause
|
||||
> What caused the issue?
|
||||
> Regression, faulty assumption, edge case, refactor, etc.
|
||||
|
||||
ex:
|
||||
|
||||
- `frontend`
|
||||
- `backend`
|
||||
- `devops`
|
||||
- `bug`
|
||||
- `enhancement`
|
||||
- `ui`
|
||||
- `test`
|
||||
#### Fix Strategy
|
||||
> How does this PR address the root cause?
|
||||
|
||||
---
|
||||
|
||||
## 👥 Reviewers
|
||||
### 🧪 Testing Strategy
|
||||
> How was this change validated?
|
||||
|
||||
> Tag the relevant teams for review:
|
||||
|
||||
- frontend / backend / devops
|
||||
- Tests added/updated:
|
||||
- Manual verification:
|
||||
- Edge cases covered:
|
||||
|
||||
---
|
||||
|
||||
## 🧪 How to Test
|
||||
### ⚠️ Risk & Impact Assessment
|
||||
> What could break? How do we recover?
|
||||
|
||||
<!-- Describe how reviewers can test this PR -->
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
- Blast radius:
|
||||
- Potential regressions:
|
||||
- Rollback plan:
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Related Issues
|
||||
### 📝 Changelog
|
||||
> Fill only if this affects users, APIs, UI, or documented behavior
|
||||
> Use **N/A** for internal or non-user-facing changes
|
||||
|
||||
<!-- Reference any related issues (e.g. Fixes #123, Closes #456) -->
|
||||
Closes #
|
||||
| Field | Value |
|
||||
|------|-------|
|
||||
| Deployment Type | Cloud / OSS / Enterprise |
|
||||
| Change Type | Feature / Bug Fix / Maintenance |
|
||||
| Description | User-facing summary |
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots / Screen Recording (if applicable / mandatory for UI related changes)
|
||||
|
||||
<!-- Add screenshots or GIFs to help visualize changes -->
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checklist
|
||||
|
||||
- [ ] Dev Review
|
||||
- [ ] Test cases added (Unit/ Integration / E2E)
|
||||
- [ ] Manually tested the changes
|
||||
|
||||
### 📋 Checklist
|
||||
- [ ] Tests added or explicitly not required
|
||||
- [ ] Manually tested
|
||||
- [ ] Breaking changes documented
|
||||
- [ ] Backward compatibility considered
|
||||
|
||||
---
|
||||
|
||||
## 👀 Notes for Reviewers
|
||||
|
||||
<!-- Anything reviewers should keep in mind while reviewing -->
|
||||
|
||||
---
|
||||
|
||||
18
.github/workflows/integrationci.yaml
vendored
18
.github/workflows/integrationci.yaml
vendored
@@ -21,11 +21,11 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
- name: poetry
|
||||
- name: uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: install
|
||||
run: |
|
||||
python -m pip install poetry==2.1.2
|
||||
python -m poetry config virtualenvs.in-project true
|
||||
cd tests/integration && poetry install --no-root
|
||||
cd tests/integration && uv sync
|
||||
- name: fmt
|
||||
run: |
|
||||
make py-fmt
|
||||
@@ -67,11 +67,11 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
- name: poetry
|
||||
- name: uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: install
|
||||
run: |
|
||||
python -m pip install poetry==2.1.2
|
||||
python -m poetry config virtualenvs.in-project true
|
||||
cd tests/integration && poetry install --no-root
|
||||
cd tests/integration && uv sync
|
||||
- name: webdriver
|
||||
run: |
|
||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: run
|
||||
run: |
|
||||
cd tests/integration && \
|
||||
poetry run pytest \
|
||||
uv run pytest \
|
||||
--basetemp=./tmp/ \
|
||||
src/${{matrix.src}} \
|
||||
--sqlstore-provider ${{matrix.sqlstore-provider}} \
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,7 +12,6 @@ frontend/coverage
|
||||
|
||||
# production
|
||||
frontend/build
|
||||
frontend/.vscode
|
||||
frontend/.yarnclean
|
||||
frontend/.temp_cache
|
||||
frontend/test-results
|
||||
@@ -31,7 +30,6 @@ frontend/src/constants/env.ts
|
||||
|
||||
.idea
|
||||
|
||||
**/.vscode
|
||||
**/build
|
||||
**/storage
|
||||
**/locust-scripts/__pycache__/
|
||||
@@ -222,8 +220,6 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
10
.mockery.yml
Normal file
10
.mockery.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Link to template variables: https://pkg.go.dev/github.com/vektra/mockery/v3/config#TemplateData
|
||||
template: testify
|
||||
packages:
|
||||
github.com/SigNoz/signoz/pkg/alertmanager:
|
||||
config:
|
||||
all: true
|
||||
dir: '{{.InterfaceDir}}/mocks'
|
||||
filename: "mocks.go"
|
||||
structname: 'Mock{{.InterfaceName}}'
|
||||
pkgname: '{{.SrcPackageName}}mock'
|
||||
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"eslint.workingDirectories": ["./frontend"],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true
|
||||
}
|
||||
14
Makefile
14
Makefile
@@ -202,25 +202,25 @@ docker-buildx-enterprise: go-build-enterprise js-build
|
||||
##############################################################
|
||||
.PHONY: py-fmt
|
||||
py-fmt: ## Run black for integration tests
|
||||
@cd tests/integration && poetry run black .
|
||||
@cd tests/integration && uv run black .
|
||||
|
||||
.PHONY: py-lint
|
||||
py-lint: ## Run lint for integration tests
|
||||
@cd tests/integration && poetry run isort .
|
||||
@cd tests/integration && poetry run autoflake .
|
||||
@cd tests/integration && poetry run pylint .
|
||||
@cd tests/integration && uv run isort .
|
||||
@cd tests/integration && uv run autoflake .
|
||||
@cd tests/integration && uv run pylint .
|
||||
|
||||
.PHONY: py-test-setup
|
||||
py-test-setup: ## Runs integration tests
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --reuse --capture=no src/bootstrap/setup.py::test_setup
|
||||
@cd tests/integration && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no src/bootstrap/setup.py::test_setup
|
||||
|
||||
.PHONY: py-test-teardown
|
||||
py-test-teardown: ## Runs integration tests with teardown
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --teardown --capture=no src/bootstrap/setup.py::test_teardown
|
||||
@cd tests/integration && uv run pytest --basetemp=./tmp/ -vv --teardown --capture=no src/bootstrap/setup.py::test_teardown
|
||||
|
||||
.PHONY: py-test
|
||||
py-test: ## Runs integration tests
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --capture=no src/
|
||||
@cd tests/integration && uv run pytest --basetemp=./tmp/ -vv --capture=no src/
|
||||
|
||||
.PHONY: py-clean
|
||||
py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
|
||||
4
context7.json
Normal file
4
context7.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"url": "https://context7.com/signoz/signoz",
|
||||
"public_key": "pk_6g9GfjdkuPEIDuTGAxnol"
|
||||
}
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.106.0
|
||||
image: signoz/signoz:v0.107.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.106.0
|
||||
image: signoz/signoz:v0.107.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.106.0}
|
||||
image: signoz/signoz:${VERSION:-v0.107.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.106.0}
|
||||
image: signoz/signoz:${VERSION:-v0.107.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -9,7 +9,7 @@ SigNoz uses integration tests to verify that different components work together
|
||||
Before running integration tests, ensure you have the following installed:
|
||||
|
||||
- Python 3.13+
|
||||
- Poetry (for dependency management)
|
||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
- Docker (for containerized services)
|
||||
|
||||
### Initial Setup
|
||||
@@ -19,17 +19,19 @@ Before running integration tests, ensure you have the following installed:
|
||||
cd tests/integration
|
||||
```
|
||||
|
||||
2. Install dependencies using Poetry:
|
||||
2. Install dependencies using uv:
|
||||
```bash
|
||||
poetry install --no-root
|
||||
uv sync
|
||||
```
|
||||
|
||||
> **_NOTE:_** the build backend could throw an error while installing `psycopg2`, pleae see https://www.psycopg.org/docs/install.html#build-prerequisites
|
||||
|
||||
### Starting the Test Environment
|
||||
|
||||
To spin up all the containers necessary for writing integration tests and keep them running:
|
||||
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/setup.py::test_setup
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/setup.py::test_setup
|
||||
```
|
||||
|
||||
This command will:
|
||||
@@ -42,7 +44,7 @@ This command will:
|
||||
When you're done writing integration tests, clean up the environment:
|
||||
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --teardown -s src/bootstrap/setup.py::test_teardown
|
||||
uv run pytest --basetemp=./tmp/ -vv --teardown -s src/bootstrap/setup.py::test_teardown
|
||||
```
|
||||
|
||||
This will destroy the running integration test setup and clean up resources.
|
||||
@@ -72,20 +74,20 @@ Python and pytest form the foundation of the integration testing framework. Test
|
||||
│ ├── sqlite.py
|
||||
│ ├── types.py
|
||||
│ └── zookeeper.py
|
||||
├── poetry.lock
|
||||
├── uv.lock
|
||||
├── pyproject.toml
|
||||
└── src
|
||||
└── bootstrap
|
||||
├── __init__.py
|
||||
├── a_database.py
|
||||
├── b_register.py
|
||||
└── c_license.py
|
||||
├── 01_database.py
|
||||
├── 02_register.py
|
||||
└── 03_license.py
|
||||
```
|
||||
|
||||
Each test suite follows some important principles:
|
||||
|
||||
1. **Organization**: Test suites live under `src/` in self-contained packages. Fixtures (a pytest concept) live inside `fixtures/`.
|
||||
2. **Execution Order**: Files are prefixed with `a_`, `b_`, `c_` to ensure sequential execution.
|
||||
2. **Execution Order**: Files are prefixed with two-digit numbers (`01_`, `02_`, `03_`) to ensure sequential execution.
|
||||
3. **Time Constraints**: Each suite should complete in under 10 minutes (setup takes ~4 mins).
|
||||
|
||||
### Test Suite Design
|
||||
@@ -107,7 +109,7 @@ Other test suites can be **pipelines, auth, querier.**
|
||||
|
||||
## How to write an integration test?
|
||||
|
||||
Now start writing an integration test. Create a new file `src/bootstrap/e_version.py` and paste the following:
|
||||
Now start writing an integration test. Create a new file `src/bootstrap/05_version.py` and paste the following:
|
||||
|
||||
```python
|
||||
import requests
|
||||
@@ -125,7 +127,7 @@ def test_version(signoz: types.SigNoz) -> None:
|
||||
We have written a simple test which calls the `version` endpoint of the container in step 1. In **order to just run this function, run the following command:**
|
||||
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/e_version.py::test_version
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/05_version.py::test_version
|
||||
```
|
||||
|
||||
> Note: The `--reuse` flag is used to reuse the environment if it is already running. Always use this flag when writing and running integration tests. If you don't use this flag, the environment will be destroyed and recreated every time you run the test.
|
||||
@@ -153,7 +155,7 @@ def test_user_registration(signoz: types.SigNoz) -> None:
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["setupCompleted"] is True
|
||||
```
|
||||
@@ -163,27 +165,27 @@ def test_user_registration(signoz: types.SigNoz) -> None:
|
||||
### Running All Tests
|
||||
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/
|
||||
```
|
||||
|
||||
### Running Specific Test Categories
|
||||
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/<suite>
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/<suite>
|
||||
|
||||
# Run querier tests
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/querier/
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/querier/
|
||||
# Run auth tests
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/auth/
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/auth/
|
||||
```
|
||||
|
||||
### Running Individual Tests
|
||||
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/<suite>/<file>.py::test_name
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/<suite>/<file>.py::test_name
|
||||
|
||||
# Run test_register in file a_register.py in auth suite
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse src/auth/a_register.py::test_register
|
||||
# Run test_register in file 01_register.py in passwordauthn suite
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse src/passwordauthn/01_register.py::test_register
|
||||
```
|
||||
|
||||
## How to configure different options for integration tests?
|
||||
@@ -197,7 +199,7 @@ Tests can be configured using pytest options:
|
||||
|
||||
Example:
|
||||
```bash
|
||||
poetry run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --postgres-version=14 src/auth/
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --postgres-version=14 src/auth/
|
||||
```
|
||||
|
||||
|
||||
@@ -205,7 +207,7 @@ poetry run pytest --basetemp=./tmp/ -vv --reuse --sqlstore-provider=postgres --p
|
||||
|
||||
- **Always use the `--reuse` flag** when setting up the environment to keep containers running
|
||||
- **Use the `--teardown` flag** when cleaning up to avoid resource leaks
|
||||
- **Follow the naming convention** with alphabetical prefixes for test execution order
|
||||
- **Follow the naming convention** with two-digit numeric prefixes (`01_`, `02_`) for test execution order
|
||||
- **Use proper timeouts** in HTTP requests to avoid hanging tests
|
||||
- **Clean up test data** between tests to avoid interference
|
||||
- **Use descriptive test names** that clearly indicate what is being tested
|
||||
|
||||
@@ -163,7 +163,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
}
|
||||
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
err := module.DeletePublic(ctx, orgID, id)
|
||||
err := module.deletePublic(ctx, orgID, id)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
@@ -263,3 +263,35 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
|
||||
publicDashboard, err := module.store.GetPublic(ctx, dashboardID.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionObject := authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
Type: authtypes.TypeMetaResource,
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
|
||||
@@ -104,6 +105,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
signoz.Alertmanager,
|
||||
signoz.SQLStore,
|
||||
signoz.TelemetryStore,
|
||||
signoz.TelemetryMetadataStore,
|
||||
signoz.Prometheus,
|
||||
signoz.Modules.OrgGetter,
|
||||
signoz.Querier,
|
||||
@@ -355,12 +357,13 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
// create manager opts
|
||||
managerOpts := &baserules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Logger: zap.L(),
|
||||
@@ -376,6 +379,7 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SqlStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
}
|
||||
|
||||
// create Manager
|
||||
|
||||
@@ -247,7 +247,8 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
}
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
SendUnmatched: r.ShouldSendUnmatched(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -291,7 +292,19 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
r.logger.InfoContext(ctx, "anomaly scores", "scores", string(scoresJSON))
|
||||
|
||||
for _, series := range queryResult.AnomalyScores {
|
||||
// Filter out new series if newGroupEvalDelay is configured
|
||||
seriesToProcess := queryResult.AnomalyScores
|
||||
if r.ShouldSkipNewGroups() {
|
||||
filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, seriesToProcess)
|
||||
// In case of error we log the error and continue with the original series
|
||||
if filterErr != nil {
|
||||
r.logger.ErrorContext(ctx, "Error filtering new series, ", "error", filterErr, "rule_name", r.Name())
|
||||
} else {
|
||||
seriesToProcess = filteredSeries
|
||||
}
|
||||
}
|
||||
|
||||
for _, series := range seriesToProcess {
|
||||
if r.Condition().RequireMinPoints {
|
||||
if len(series.Points) < r.Condition().RequiredNumPoints {
|
||||
r.logger.InfoContext(ctx, "not enough data points to evaluate series, skipping", "ruleid", r.ID(), "numPoints", len(series.Points), "requiredPoints", r.Condition().RequiredNumPoints)
|
||||
@@ -299,7 +312,8 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
}
|
||||
results, err := r.Threshold.Eval(*series, r.Unit(), ruletypes.EvalData{
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
ActiveAlerts: r.ActiveAlertsLabelFP(),
|
||||
SendUnmatched: r.ShouldSendUnmatched(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -37,6 +37,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
opts.SLogger,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -59,6 +61,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
opts.Reader,
|
||||
opts.ManagerOpts.Prometheus,
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -82,6 +86,8 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
opts.Cache,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
)
|
||||
if err != nil {
|
||||
return task, err
|
||||
@@ -140,6 +146,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -160,6 +168,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -179,6 +189,8 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
baserules.WithSendAlways(),
|
||||
baserules.WithSendUnmatched(),
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", alertname), zap.Error(err))
|
||||
|
||||
283
ee/query-service/rules/manager_test.go
Normal file
283
ee/query-service/rules/manager_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
alertmanagermock "github.com/SigNoz/signoz/pkg/alertmanager/mocks"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/prometheustest"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
cmock "github.com/srikanthccv/ClickHouse-go-mock"
|
||||
)
|
||||
|
||||
func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
|
||||
target := 10.0
|
||||
recovery := 5.0
|
||||
|
||||
for _, tc := range rules.TcTestNotiSendUnmatchedThresholdRule {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
rule := rules.ThresholdRuleAtLeastOnceValueAbove(target, &recovery)
|
||||
|
||||
// Marshal rule to JSON as TestNotification expects
|
||||
ruleBytes, err := json.Marshal(rule)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
// for saving temp alerts that are triggered via TestNotification
|
||||
triggeredTestAlerts := []map[*alertmanagertypes.PostableAlert][]string{}
|
||||
|
||||
// Create manager using test factory with hooks
|
||||
mgr := rules.NewTestManager(t, &rules.TestManagerOptions{
|
||||
AlertmanagerHook: func(am alertmanager.Alertmanager) {
|
||||
fAlert := am.(*alertmanagermock.MockAlertmanager)
|
||||
// mock set notification config
|
||||
fAlert.On("SetNotificationConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
// for saving temp alerts that are triggered via TestNotification
|
||||
if tc.ExpectAlerts > 0 {
|
||||
fAlert.On("TestAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
triggeredTestAlerts = append(triggeredTestAlerts, args.Get(3).(map[*alertmanagertypes.PostableAlert][]string))
|
||||
}).Return(nil).Times(tc.ExpectAlerts)
|
||||
}
|
||||
},
|
||||
ManagerOptionsHook: func(opts *rules.ManagerOptions) {
|
||||
opts.PrepareTestRuleFunc = TestNotification
|
||||
},
|
||||
SqlStoreHook: func(store sqlstore.SQLStore) {
|
||||
mockStore := store.(*sqlstoretest.Provider)
|
||||
// Mock the organizations query that SendAlerts makes
|
||||
// Bun generates: SELECT id FROM organizations LIMIT 1 (or SELECT "id" FROM "organizations" LIMIT 1)
|
||||
orgRows := mockStore.Mock().NewRows([]string{"id"}).AddRow(orgID.StringValue())
|
||||
// Match bun's generated query pattern - bun may quote identifiers
|
||||
mockStore.Mock().ExpectQuery("SELECT (.+) FROM (.+)organizations(.+) LIMIT (.+)").WillReturnRows(orgRows)
|
||||
},
|
||||
TelemetryStoreHook: func(store telemetrystore.TelemetryStore) {
|
||||
telemetryStore := store.(*telemetrystoretest.Provider)
|
||||
// Set up mock data for telemetry store
|
||||
cols := make([]cmock.ColumnType, 0)
|
||||
cols = append(cols, cmock.ColumnType{Name: "value", Type: "Float64"})
|
||||
cols = append(cols, cmock.ColumnType{Name: "attr", Type: "String"})
|
||||
cols = append(cols, cmock.ColumnType{Name: "ts", Type: "DateTime"})
|
||||
|
||||
alertDataRows := cmock.NewRows(cols, tc.Values)
|
||||
|
||||
mock := telemetryStore.Mock()
|
||||
|
||||
// Generate query arguments for the metric query
|
||||
evalTime := time.Now().UTC()
|
||||
evalWindow := 5 * time.Minute
|
||||
evalDelay := time.Duration(0)
|
||||
queryArgs := rules.GenerateMetricQueryCHArgs(
|
||||
evalTime,
|
||||
evalWindow,
|
||||
evalDelay,
|
||||
"probe_success",
|
||||
metrictypes.Unspecified,
|
||||
)
|
||||
|
||||
mock.ExpectQuery("*WITH __temporal_aggregation_cte*").
|
||||
WithArgs(queryArgs...).
|
||||
WillReturnRows(alertDataRows)
|
||||
},
|
||||
})
|
||||
|
||||
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
if apiErr != nil {
|
||||
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
|
||||
}
|
||||
require.Nil(t, apiErr)
|
||||
assert.Equal(t, tc.ExpectAlerts, count)
|
||||
|
||||
if tc.ExpectAlerts > 0 {
|
||||
// check if the alert has been triggered
|
||||
require.Len(t, triggeredTestAlerts, 1)
|
||||
var gotAlerts []*alertmanagertypes.PostableAlert
|
||||
for a := range triggeredTestAlerts[0] {
|
||||
gotAlerts = append(gotAlerts, a)
|
||||
}
|
||||
require.Len(t, gotAlerts, tc.ExpectAlerts)
|
||||
// check if the alert has triggered with correct threshold value
|
||||
if tc.ExpectValue != 0 {
|
||||
assert.Equal(t, strconv.FormatFloat(tc.ExpectValue, 'f', -1, 64), gotAlerts[0].Annotations["value"])
|
||||
}
|
||||
} else {
|
||||
// check if no alerts have been triggered
|
||||
assert.Empty(t, triggeredTestAlerts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
|
||||
target := 10.0
|
||||
|
||||
for _, tc := range rules.TcTestNotificationSendUnmatchedPromRule {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
// Capture base time once per test case to ensure consistent timestamps
|
||||
baseTime := time.Now().UTC()
|
||||
|
||||
rule := rules.BuildPromAtLeastOnceValueAbove(target, nil)
|
||||
|
||||
// Marshal rule to JSON as TestNotification expects
|
||||
ruleBytes, err := json.Marshal(rule)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
// for saving temp alerts that are triggered via TestNotification
|
||||
triggeredTestAlerts := []map[*alertmanagertypes.PostableAlert][]string{}
|
||||
|
||||
// Variable to store promProvider for cleanup
|
||||
var promProvider *prometheustest.Provider
|
||||
|
||||
// Create manager using test factory with hooks
|
||||
mgr := rules.NewTestManager(t, &rules.TestManagerOptions{
|
||||
AlertmanagerHook: func(am alertmanager.Alertmanager) {
|
||||
mockAM := am.(*alertmanagermock.MockAlertmanager)
|
||||
// mock set notification config
|
||||
mockAM.On("SetNotificationConfig", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
// for saving temp alerts that are triggered via TestNotification
|
||||
if tc.ExpectAlerts > 0 {
|
||||
mockAM.On("TestAlert", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
triggeredTestAlerts = append(triggeredTestAlerts, args.Get(3).(map[*alertmanagertypes.PostableAlert][]string))
|
||||
}).Return(nil).Times(tc.ExpectAlerts)
|
||||
}
|
||||
},
|
||||
SqlStoreHook: func(store sqlstore.SQLStore) {
|
||||
mockStore := store.(*sqlstoretest.Provider)
|
||||
// Mock the organizations query that SendAlerts makes
|
||||
orgRows := mockStore.Mock().NewRows([]string{"id"}).AddRow(orgID.StringValue())
|
||||
mockStore.Mock().ExpectQuery("SELECT (.+) FROM (.+)organizations(.+) LIMIT (.+)").WillReturnRows(orgRows)
|
||||
},
|
||||
TelemetryStoreHook: func(store telemetrystore.TelemetryStore) {
|
||||
mockStore := store.(*telemetrystoretest.Provider)
|
||||
|
||||
// Set up Prometheus-specific mock data
|
||||
// Fingerprint columns for Prometheus queries
|
||||
fingerprintCols := []cmock.ColumnType{
|
||||
{Name: "fingerprint", Type: "UInt64"},
|
||||
{Name: "any(labels)", Type: "String"},
|
||||
}
|
||||
|
||||
// Samples columns for Prometheus queries
|
||||
samplesCols := []cmock.ColumnType{
|
||||
{Name: "metric_name", Type: "String"},
|
||||
{Name: "fingerprint", Type: "UInt64"},
|
||||
{Name: "unix_milli", Type: "Int64"},
|
||||
{Name: "value", Type: "Float64"},
|
||||
{Name: "flags", Type: "UInt32"},
|
||||
}
|
||||
|
||||
// Calculate query time range similar to Prometheus rule tests
|
||||
// TestNotification uses time.Now().UTC() for evaluation
|
||||
// We calculate the query window based on current time to match what the actual evaluation will use
|
||||
evalTime := baseTime
|
||||
evalWindowMs := int64(5 * 60 * 1000) // 5 minutes in ms
|
||||
evalTimeMs := evalTime.UnixMilli()
|
||||
queryStart := ((evalTimeMs-2*evalWindowMs)/60000)*60000 + 1 // truncate to minute + 1ms
|
||||
queryEnd := (evalTimeMs / 60000) * 60000 // truncate to minute
|
||||
|
||||
// Create fingerprint data
|
||||
fingerprint := uint64(12345)
|
||||
labelsJSON := `{"__name__":"test_metric"}`
|
||||
fingerprintData := [][]interface{}{
|
||||
{fingerprint, labelsJSON},
|
||||
}
|
||||
fingerprintRows := cmock.NewRows(fingerprintCols, fingerprintData)
|
||||
|
||||
// Create samples data from test case values, calculating timestamps relative to baseTime
|
||||
validSamplesData := make([][]interface{}, 0)
|
||||
for _, v := range tc.Values {
|
||||
// Skip NaN and Inf values in the samples data
|
||||
if math.IsNaN(v.Value) || math.IsInf(v.Value, 0) {
|
||||
continue
|
||||
}
|
||||
// Calculate timestamp relative to baseTime
|
||||
sampleTimestamp := baseTime.Add(v.Offset).UnixMilli()
|
||||
validSamplesData = append(validSamplesData, []interface{}{
|
||||
"test_metric",
|
||||
fingerprint,
|
||||
sampleTimestamp,
|
||||
v.Value,
|
||||
uint32(0), // flags - 0 means normal value
|
||||
})
|
||||
}
|
||||
samplesRows := cmock.NewRows(samplesCols, validSamplesData)
|
||||
|
||||
mock := mockStore.Mock()
|
||||
|
||||
// Mock the fingerprint query (for Prometheus label matching)
|
||||
mock.ExpectQuery("SELECT fingerprint, any").
|
||||
WithArgs("test_metric", "__name__", "test_metric").
|
||||
WillReturnRows(fingerprintRows)
|
||||
|
||||
// Mock the samples query (for Prometheus metric data)
|
||||
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
|
||||
WithArgs(
|
||||
"test_metric",
|
||||
"test_metric",
|
||||
"__name__",
|
||||
"test_metric",
|
||||
queryStart,
|
||||
queryEnd,
|
||||
).
|
||||
WillReturnRows(samplesRows)
|
||||
|
||||
// Create Prometheus provider for this test
|
||||
promProvider = prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, store)
|
||||
},
|
||||
ManagerOptionsHook: func(opts *rules.ManagerOptions) {
|
||||
// Set Prometheus provider for PromQL queries
|
||||
if promProvider != nil {
|
||||
opts.Prometheus = promProvider
|
||||
}
|
||||
opts.PrepareTestRuleFunc = TestNotification
|
||||
},
|
||||
})
|
||||
|
||||
count, apiErr := mgr.TestNotification(context.Background(), orgID, string(ruleBytes))
|
||||
if apiErr != nil {
|
||||
t.Logf("TestNotification error: %v, type: %s", apiErr.Err, apiErr.Typ)
|
||||
}
|
||||
require.Nil(t, apiErr)
|
||||
assert.Equal(t, tc.ExpectAlerts, count)
|
||||
|
||||
if tc.ExpectAlerts > 0 {
|
||||
// check if the alert has been triggered
|
||||
require.Len(t, triggeredTestAlerts, 1)
|
||||
var gotAlerts []*alertmanagertypes.PostableAlert
|
||||
for a := range triggeredTestAlerts[0] {
|
||||
gotAlerts = append(gotAlerts, a)
|
||||
}
|
||||
require.Len(t, gotAlerts, tc.ExpectAlerts)
|
||||
// check if the alert has triggered with correct threshold value
|
||||
if tc.ExpectValue != 0 && !math.IsNaN(tc.ExpectValue) && !math.IsInf(tc.ExpectValue, 0) {
|
||||
assert.Equal(t, strconv.FormatFloat(tc.ExpectValue, 'f', -1, 64), gotAlerts[0].Annotations["value"])
|
||||
}
|
||||
} else {
|
||||
// check if no alerts have been triggered
|
||||
assert.Empty(t, triggeredTestAlerts)
|
||||
}
|
||||
|
||||
promProvider.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ module.exports = {
|
||||
'jest/globals': true,
|
||||
},
|
||||
extends: [
|
||||
'airbnb',
|
||||
'airbnb-typescript',
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
@@ -35,6 +33,7 @@ module.exports = {
|
||||
'react-hooks',
|
||||
'prettier',
|
||||
'jest',
|
||||
'jsx-a11y',
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
@@ -72,9 +71,6 @@ module.exports = {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
|
||||
// airbnb
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-console': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
@@ -87,6 +83,9 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
// Disabled because TypeScript already handles this check more accurately,
|
||||
// and the rule has false positives with type-only imports (e.g., TooltipProps from antd)
|
||||
'import/named': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'jsx-a11y/label-has-associated-control': [
|
||||
'error',
|
||||
@@ -104,7 +103,10 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
// Allow empty functions for mocks, default context values, and noop callbacks
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
// Allow underscore prefix for intentionally unused variables (e.g., const { id: _id, ...rest } = props)
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
|
||||
|
||||
@@ -4,5 +4,14 @@
|
||||
"tabWidth": 1,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"semi": true
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"quoteProps": "as-needed",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
|
||||
8
frontend/.vscode/settings.json
vendored
Normal file
8
frontend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"prettier.requireConfig": true
|
||||
}
|
||||
@@ -34,12 +34,18 @@ Embrace the spirit of collaboration and contribute to the success of our open-so
|
||||
### Linting and Setup
|
||||
|
||||
- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase.
|
||||
- In our project, we rely on several essential ESLint plugins, namely:
|
||||
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/)
|
||||
- [airbnb styleguide](https://github.com/airbnb/javascript)
|
||||
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs)
|
||||
- In our project, we rely on several essential ESLint plugins and configurations:
|
||||
|
||||
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
|
||||
- [eslint:recommended](https://eslint.org/docs/latest/rules/) - Core ESLint rules for JavaScript best practices
|
||||
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/) - TypeScript-specific linting rules
|
||||
- [plugin:react](https://github.com/jsx-eslint/eslint-plugin-react) - React best practices and patterns
|
||||
- [plugin:react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) - Rules of Hooks enforcement
|
||||
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) - Code quality and complexity analysis
|
||||
- [plugin:prettier](https://github.com/prettier/eslint-plugin-prettier) - Code formatting via Prettier
|
||||
- [simple-import-sort](https://github.com/lydell/eslint-plugin-simple-import-sort) - Automatic import organization
|
||||
- [plugin:jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - Accessibility rules for JSX elements
|
||||
|
||||
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
|
||||
@@ -219,16 +219,11 @@
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jest": "^26.9.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||
"ALERT_TYPE_SELECTION": "SigNoz | Select Alert Type",
|
||||
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
|
||||
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
|
||||
"METER_EXPLORER": "SigNoz | Meter Explorer",
|
||||
|
||||
1
frontend/public/svgs/barber-pool.svg
Normal file
1
frontend/public/svgs/barber-pool.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path fill="#fff" d="m9.05 20.616 13.968-.178-.06-9.283-14.417.178z"/><path fill="#d80d1a" d="m10.165 20.736-1.302-2.253.127-5.209 5.133 7.491z"/><path fill="#1d86fa" d="m16.72 20.616-6.416-9.401 4.298.06 6.536 9.372z"/><path fill="#d80d1a" d="m22.958 19.125-5.58-7.88 4.118.12 2.12 2.956z"/><path fill="#a1d2d6" d="m8.75 11.722 14.208-.09-.098-.86-14.14.114zM8.99 21.214l14.237-.18-.329-.864-14.237.177z"/><path fill="#c8c8c8" d="M22.772 21.163c.122.147.882.175.882.175s.444.825 1.41.707c.865-.106 1.2-.829 1.2-.829s1.463-.338 2.259-1.464c.846-1.2.882-2.383.882-4.076 0-1.836-.143-3.088-1.076-4.076-1.073-1.134-2.186-1.218-2.186-1.218s-.39-.654-1.271-.636c-.883.018-1.218.74-1.218.74s-.704.151-.882.282c-.091.067-.067 2.703-.054 5.294.012 2.5-.042 4.988.054 5.1"/><path fill="#858585" d="M28.541 16.538c.353.018.345-1.851.036-3.229-.354-1.57-1.694-2.082-1.8-1.993s-.071 1.675.017 1.764c.09.09.918.494 1.2 1.182.283.69.278 2.262.547 2.276M25.299 21.99l-.056-12.213s.258.04.509.207c.242.162.389.395.389.395l.127 10.828s-.176.325-.35.465c-.381.309-.62.318-.62.318"/><path fill="#e1e0e0" d="m24.692 22.043-.157-12.255s-.323.04-.565.258c-.204.182-.318.44-.318.44l.107 11.01s.164.223.353.354c.23.157.58.193.58.193"/><path fill="#c8c8c8" d="M9.13 21.32c.158-.142.036-10.216-.053-10.392-.089-.175-.829-1.069-2.01-1-1.167.069-1.536.876-1.536.876s-1.169.155-2.065 1.5c-.635.954-.82 2.19-.806 3.74.015 1.712.12 3.319 1.142 4.341.982.982 1.94 1.022 1.94 1.022s.267.87 1.573.87c1.289.002 1.816-.956 1.816-.956"/><path fill="#858585" d="M7.548 22.274s-.373-1.907-.427-6.158.256-6.186.256-6.186.258-.017.504.067c.276.093.458.24.458.24s-.244 3.656-.227 5.879c.018 2.222.37 5.85.37 5.85s-.265.153-.423.206c-.164.051-.51.102-.51.102"/><path fill="#e1e0e0" d="M6.858 22.234s-.48-2.45-.533-6.032.157-6.163.157-6.163-.422.127-.633.322c-.21.196-.318.44-.318.44s-.122 3.359-.14 5.39c-.02 2.454.351 5.216.351 5.216s.085.296.345.496c.367.285.771.331.771.331"/><path fill="#fff" d="M6.1 16.763c.19 0 .3 1.251.38 2.58.045.744.411 2.058-.08 2.058-.49 0-.584-.476-.617-2.09-.034-1.612.064-2.548.317-2.548M3.173 16.667c.12-.042.35.634.903 1.156.39.369.76.49.855.744.096.254.222 1.613-.062 1.631-.396.023-1.022-.262-1.393-.933-.59-1.062-.525-2.518-.303-2.598"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
frontend/public/svgs/tv.svg
Normal file
1
frontend/public/svgs/tv.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"><path fill="#455a64" d="m15.255 10.911-4.82-6.048a.204.204 0 0 1 .025-.278.2.2 0 0 1 .284.018l5.127 5.775a.2.2 0 0 1-.018.284l-.307.271a.2.2 0 0 1-.291-.022M16.494 10.878l-.276-.245a.22.22 0 0 1-.017-.313l5.097-5.746a.22.22 0 0 1 .313-.018c.09.078.1.213.027.307l-4.822 5.99a.227.227 0 0 1-.322.025"/><path fill="#455a64" d="M12.389 11.214c0-.775 1.72-1.406 3.842-1.406s3.842.629 3.842 1.406z"/><path fill="#82aec0" d="M28.353 29.337H3.652c-.544 0-.985-.458-.985-1.02V11.8c0-.564.44-1.02.985-1.02h24.703c.542 0 .985.458.985 1.02v16.516c-.003.562-.443 1.02-.987 1.02"/><path fill="#212121" d="M27.637 28.368H4.438a.79.79 0 0 1-.789-.79V12.456a.79.79 0 0 1 .79-.79h23.198c.436 0 .789.354.789.79v15.124a.79.79 0 0 1-.79.789"/><path fill="#b9e4ea" d="m5.2 25.995-1.068 1.677c-.186.293.014.691.36.694h18.414a.536.536 0 0 0 .478-.77l-.667-1.373z"/><path fill="#455a64" d="M27.312 27.662h-2.53a.464.464 0 0 1-.465-.464V21.92c0-.258.209-.464.464-.464h2.531c.258 0 .465.209.465.464v5.278a.464.464 0 0 1-.465.464M26.048 20.226a1.531 1.531 0 1 0 0-3.062 1.531 1.531 0 0 0 0 3.062"/><path fill="#82aec0" d="M24.966 17.798a.254.254 0 0 0-.009.313l.504.678c.158.21.363.382.6.497l.76.37a.254.254 0 0 0 .316-.38l-.505-.678a1.65 1.65 0 0 0-.6-.498l-.76-.369a.26.26 0 0 0-.306.067"/><path fill="#455a64" d="M26.048 16.268a1.531 1.531 0 1 0 0-3.062 1.531 1.531 0 0 0 0 3.062"/><path fill="#82aec0" d="M26.049 13.33c-.118 0-.22.08-.247.194l-.2.822a1.65 1.65 0 0 0 0 .78l.2.822a.254.254 0 0 0 .493 0l.2-.822a1.65 1.65 0 0 0 0-.78l-.2-.822a.256.256 0 0 0-.247-.194"/><path fill="#2f7889" d="M22.992 25.219c-.17.77-.742 1.366-1.475 1.529-1.205.269-3.45.604-7.148.604-3.883 0-6.487-.369-7.89-.642-.775-.151-1.384-.787-1.536-1.607-.348-1.885-.786-5.652.018-10.357.15-.871.814-1.54 1.643-1.653 1.495-.207 4.167-.483 7.765-.483 3.414 0 5.7.25 6.997.45.797.124 1.435.77 1.598 1.608.962 4.957.43 8.733.028 10.55"/><path fill="#212121" fill-rule="evenodd" d="M14.37 13.055c-3.576 0-6.227.274-7.705.478-.623.085-1.147.593-1.265 1.288-.794 4.64-.361 8.353-.02 10.201.121.653.6 1.138 1.184 1.252 1.375.267 3.951.634 7.805.634 3.672 0 5.884-.334 7.051-.594.553-.122 1.002-.576 1.139-1.192.392-1.772.917-5.485-.032-10.369-.13-.67-.633-1.161-1.23-1.254-1.272-.197-3.536-.444-6.928-.444m-7.827-.403c1.514-.209 4.206-.486 7.826-.486 3.436 0 5.746.25 7.064.454h.001c1 .156 1.771.959 1.966 1.964.976 5.028.439 8.867.027 10.73-.206.928-.9 1.665-1.814 1.868-1.242.277-3.52.615-7.244.615-3.91 0-6.544-.372-7.975-.65-.967-.19-1.705-.976-1.887-1.963m2.036-12.532c-1.035.142-1.84.972-2.02 2.02-.815 4.768-.372 8.59-.016 10.512" clip-rule="evenodd"/><path fill="url(#a)" d="M8.686 14.175c.373-.045 1.08-.013 1.329.744.248.758-.298.963-.591 1.187-.843.642-1.26.887-1.767 1.587-.404.56-1.111.384-1.322.015-.169-.298-.293-1.253.064-1.884.771-1.351 1.914-1.605 2.287-1.649"/><defs><linearGradient id="a" x1="8.303" x2="8.07" y1="9.59" y2="18.014" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -93,13 +93,15 @@ export const OnboardingV2 = Loadable(
|
||||
() => import(/* webpackChunkName: "Onboarding V2" */ 'pages/OnboardingPageV2'),
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
export const DashboardsListPage = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
||||
import(
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
|
||||
),
|
||||
);
|
||||
|
||||
export const NewDashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "New DashboardPage" */ 'pages/NewDashboard'),
|
||||
export const DashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
|
||||
);
|
||||
|
||||
export const DashboardWidget = Loadable(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
|
||||
import MessagingQueues from 'pages/MessagingQueues';
|
||||
import MeterExplorer from 'pages/MeterExplorer';
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
DashboardPage,
|
||||
DashboardsListPage,
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
@@ -27,7 +29,6 @@ import {
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MetricsExplorer,
|
||||
NewDashboardPage,
|
||||
OldLogsExplorer,
|
||||
Onboarding,
|
||||
OnboardingV2,
|
||||
@@ -159,14 +160,14 @@ const routes: AppRoutes[] = [
|
||||
{
|
||||
path: ROUTES.ALL_DASHBOARD,
|
||||
exact: true,
|
||||
component: DashboardPage,
|
||||
component: DashboardsListPage,
|
||||
isPrivate: true,
|
||||
key: 'ALL_DASHBOARD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD,
|
||||
exact: true,
|
||||
component: NewDashboardPage,
|
||||
component: DashboardPage,
|
||||
isPrivate: true,
|
||||
key: 'DASHBOARD',
|
||||
},
|
||||
@@ -198,6 +199,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'LIST_ALL_ALERT',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERT_TYPE_SELECTION,
|
||||
exact: true,
|
||||
component: AlertTypeSelectionPage,
|
||||
isPrivate: true,
|
||||
key: 'ALERT_TYPE_SELECTION',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALERTS_NEW,
|
||||
exact: true,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
QueryRangePayloadV5,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { prepareQueryRangePayloadV5 } from './prepareQueryRangePayloadV5';
|
||||
@@ -41,7 +41,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
temporality: '',
|
||||
timeAggregation: 'sum',
|
||||
spaceAggregation: 'avg',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
timeAggregation: 'sum',
|
||||
@@ -62,7 +62,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
limit: null,
|
||||
stepInterval: 600,
|
||||
orderBy: [],
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
legend: 'Legend A',
|
||||
...overrides,
|
||||
});
|
||||
@@ -416,7 +416,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
metricName: 'cpu_usage',
|
||||
timeAggregation: 'sum',
|
||||
spaceAggregation: 'avg',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
temporality: undefined,
|
||||
}),
|
||||
],
|
||||
@@ -569,7 +569,7 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
},
|
||||
],
|
||||
legend: '{{service.name}}',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
offset: 0,
|
||||
pageSize: 100,
|
||||
},
|
||||
|
||||
145
frontend/src/components/AuthError/AuthError.styles.scss
Normal file
145
frontend/src/components/AuthError/AuthError.styles.scss
Normal file
@@ -0,0 +1,145 @@
|
||||
.auth-error-container {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
animation: horizontal-shaking 300ms ease-out;
|
||||
|
||||
.error-content {
|
||||
background: rgba(229, 72, 77, 0.1);
|
||||
border: 1px solid rgba(229, 72, 77, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
&__summary-section {
|
||||
border-bottom: 1px solid rgba(229, 72, 77, 0.2);
|
||||
}
|
||||
|
||||
&__summary {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&__summary-left {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__summary-text {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__error-code {
|
||||
color: #fadadb;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
color: #f5b6b8;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
|
||||
&__message-badge {
|
||||
padding: 0px 16px 16px;
|
||||
}
|
||||
|
||||
&__message-badge-label-text {
|
||||
color: #fadadb;
|
||||
}
|
||||
|
||||
&__message-badge-line {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
rgba(229, 72, 77, 0.3) 1px,
|
||||
transparent 2px
|
||||
);
|
||||
}
|
||||
|
||||
&__messages-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__message-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
&__message-item {
|
||||
color: #f5b6b8;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
|
||||
&::before {
|
||||
background: #f5b6b8;
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-hint {
|
||||
background: rgba(229, 72, 77, 0.2);
|
||||
}
|
||||
|
||||
&__scroll-hint-text {
|
||||
color: #fadadb;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-error-icon {
|
||||
color: var(--bg-cherry-300);
|
||||
padding-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-error-container {
|
||||
.error-content {
|
||||
background: rgba(229, 72, 77, 0.1);
|
||||
border-color: rgba(229, 72, 77, 0.2);
|
||||
|
||||
&__error-code {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&__message-item {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&::before {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-hint-text {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes horizontal-shaking {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
22
frontend/src/components/AuthError/AuthError.tsx
Normal file
22
frontend/src/components/AuthError/AuthError.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import './AuthError.styles.scss';
|
||||
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import { CircleAlert } from 'lucide-react';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
interface AuthErrorProps {
|
||||
error: APIError;
|
||||
}
|
||||
|
||||
function AuthError({ error }: AuthErrorProps): JSX.Element {
|
||||
return (
|
||||
<div className="auth-error-container">
|
||||
<ErrorContent
|
||||
error={error}
|
||||
icon={<CircleAlert size={12} className="auth-error-icon" />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthError;
|
||||
115
frontend/src/components/AuthPageContainer/AuthFooter.styles.scss
Normal file
115
frontend/src/components/AuthPageContainer/AuthFooter.styles.scss
Normal file
@@ -0,0 +1,115 @@
|
||||
@import '@signozhq/design-tokens/dist/style.css';
|
||||
|
||||
.auth-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
background: var(--bg-ink-400, #121317);
|
||||
border: 1px solid var(--bg-ink-200, #23262e);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.auth-footer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.auth-footer-status-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 9999px;
|
||||
background: #25e192;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.auth-footer-icon {
|
||||
aspect-ratio: 1.93;
|
||||
width: 29px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
font-family: var(--font-family-inter, Inter, sans-serif);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-footer-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-footer-link-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-neutral-dark-50, #eceef2);
|
||||
}
|
||||
|
||||
.auth-footer-link-status {
|
||||
.auth-footer-text {
|
||||
color: #25e192;
|
||||
}
|
||||
|
||||
.auth-footer-link-icon {
|
||||
color: #25e192;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-footer-separator {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-ink-200, #23262e);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-footer-content {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.auth-footer-icon {
|
||||
filter: brightness(0) saturate(100%) invert(25%) sepia(8%) saturate(518%)
|
||||
hue-rotate(192deg) brightness(80%) contrast(95%);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.auth-footer-link-icon {
|
||||
color: var(--text-neutral-light-100, #62636c);
|
||||
}
|
||||
|
||||
.auth-footer-separator {
|
||||
background: var(--bg-vanilla-300, #e9e9e9);
|
||||
}
|
||||
}
|
||||
75
frontend/src/components/AuthPageContainer/AuthFooter.tsx
Normal file
75
frontend/src/components/AuthPageContainer/AuthFooter.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import './AuthFooter.styles.scss';
|
||||
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
interface FooterItem {
|
||||
icon?: string;
|
||||
text: string;
|
||||
url?: string;
|
||||
statusIndicator?: boolean;
|
||||
}
|
||||
|
||||
const footerItems: FooterItem[] = [
|
||||
{
|
||||
text: 'All systems operational',
|
||||
url: 'https://status.signoz.io/',
|
||||
statusIndicator: true,
|
||||
},
|
||||
{
|
||||
text: 'Privacy',
|
||||
url: 'https://www.signoz.io/privacy',
|
||||
},
|
||||
{
|
||||
text: 'Security',
|
||||
url: 'https://www.signoz.io/security',
|
||||
},
|
||||
];
|
||||
|
||||
function AuthFooter(): JSX.Element {
|
||||
return (
|
||||
<footer className="auth-footer">
|
||||
<div className="auth-footer-content">
|
||||
{footerItems.map((item, index) => (
|
||||
<React.Fragment key={item.text}>
|
||||
<div className="auth-footer-item">
|
||||
{item.statusIndicator && (
|
||||
<div className="auth-footer-status-indicator" />
|
||||
)}
|
||||
{item.icon && (
|
||||
<img
|
||||
loading="lazy"
|
||||
src={item.icon}
|
||||
alt=""
|
||||
className="auth-footer-icon"
|
||||
/>
|
||||
)}
|
||||
{item.url ? (
|
||||
<a
|
||||
href={item.url}
|
||||
className={`auth-footer-link ${
|
||||
item.statusIndicator ? 'auth-footer-link-status' : ''
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="auth-footer-text">{item.text}</span>
|
||||
{!item.statusIndicator && (
|
||||
<ArrowUpRight size={12} className="auth-footer-link-icon" />
|
||||
)}
|
||||
</a>
|
||||
) : (
|
||||
<span className="auth-footer-text">{item.text}</span>
|
||||
)}
|
||||
</div>
|
||||
{index < footerItems.length - 1 && (
|
||||
<div className="auth-footer-separator" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthFooter;
|
||||
@@ -0,0 +1,82 @@
|
||||
@import '@signozhq/design-tokens/dist/style.css';
|
||||
|
||||
.auth-header {
|
||||
width: 100%;
|
||||
max-width: 1176px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4.9px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-header-logo-icon {
|
||||
width: 17.5px;
|
||||
height: 17.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.auth-header-logo-text {
|
||||
font-family: Satoshi, var(--font-family-inter, Inter), sans-serif;
|
||||
font-size: 15.4px;
|
||||
font-weight: 500;
|
||||
line-height: 17.5px;
|
||||
color: var(--text-neutral-dark-50, #eceef2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auth-header-help-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-ink-400, #121317);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
span {
|
||||
font-family: var(--font-family-inter, Inter, sans-serif);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-neutral-dark-100, #adb4c2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-header-logo-text {
|
||||
color: var(--text-neutral-light-100, #62636c);
|
||||
}
|
||||
|
||||
.auth-header-help-button {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
|
||||
span,
|
||||
svg {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
frontend/src/components/AuthPageContainer/AuthHeader.tsx
Normal file
33
frontend/src/components/AuthPageContainer/AuthHeader.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import './AuthHeader.styles.scss';
|
||||
|
||||
import { Button } from '@signozhq/button';
|
||||
import { LifeBuoy } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
function AuthHeader(): JSX.Element {
|
||||
const handleGetHelp = useCallback((): void => {
|
||||
window.open('https://signoz.io/support/', '_blank');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="auth-header">
|
||||
<div className="auth-header-logo">
|
||||
<img
|
||||
src="/Logos/signoz-brand-logo.svg"
|
||||
alt="SigNoz"
|
||||
className="auth-header-logo-icon"
|
||||
/>
|
||||
<span className="auth-header-logo-text">SigNoz</span>
|
||||
</div>
|
||||
<Button
|
||||
className="auth-header-help-button"
|
||||
prefixIcon={<LifeBuoy size={12} />}
|
||||
onClick={handleGetHelp}
|
||||
>
|
||||
Get Help
|
||||
</Button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthHeader;
|
||||
@@ -0,0 +1,181 @@
|
||||
@import '@signozhq/design-tokens/dist/style.css';
|
||||
|
||||
.auth-page-wrapper {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background: var(--bg-neutral-dark-1000, #0a0c10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.auth-page-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-page-dots {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bg-dot-pattern {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
var(--bg-neutral-dark-50, #eceef2) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.masked-dots {
|
||||
mask-image: radial-gradient(
|
||||
circle at 50% 0%,
|
||||
rgba(11, 12, 14, 0.1) 0%,
|
||||
rgba(11, 12, 14, 0) 56.77%
|
||||
);
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle at 50% 0%,
|
||||
rgba(11, 12, 14, 0.1) 0%,
|
||||
rgba(11, 12, 14, 0) 56.77%
|
||||
);
|
||||
}
|
||||
|
||||
.auth-page-gradient {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 0 auto;
|
||||
height: 450px;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
border-radius: 956px;
|
||||
background: radial-gradient(
|
||||
ellipse at center -500px,
|
||||
rgba(78, 116, 248, 0.3) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
opacity: 0.3;
|
||||
filter: blur(150px);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
height: 956px;
|
||||
filter: blur(300px);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-page-line-left,
|
||||
.auth-page-line-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-ink-200, #23262e) 0px,
|
||||
var(--bg-ink-200, #23262e) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
pointer-events: none;
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-page-line-left {
|
||||
left: calc(50% - 600px);
|
||||
}
|
||||
|
||||
.auth-page-line-right {
|
||||
left: calc(50% + 600px);
|
||||
}
|
||||
|
||||
.auth-page-layout {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-page-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 8vh;
|
||||
padding-bottom: 24px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-top: 15vh;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.onboarding-flow {
|
||||
padding-top: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-page-wrapper {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
}
|
||||
|
||||
.bg-dot-pattern {
|
||||
background: radial-gradient(circle, rgba(35, 38, 46, 1) 1px, transparent 1px);
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
.auth-page-gradient {
|
||||
background: radial-gradient(
|
||||
ellipse at center top,
|
||||
rgba(78, 116, 248, 0.12) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
opacity: 0.8;
|
||||
filter: blur(200px);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
filter: blur(300px);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-page-line-left,
|
||||
.auth-page-line-right {
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-vanilla-300, #e9e9e9) 0px,
|
||||
var(--bg-vanilla-300, #e9e9e9) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
41
frontend/src/components/AuthPageContainer/index.tsx
Normal file
41
frontend/src/components/AuthPageContainer/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import './AuthPageContainer.styles.scss';
|
||||
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import AuthFooter from './AuthFooter';
|
||||
import AuthHeader from './AuthHeader';
|
||||
|
||||
type AuthPageContainerProps = PropsWithChildren<{
|
||||
isOnboarding?: boolean;
|
||||
}>;
|
||||
|
||||
function AuthPageContainer({
|
||||
children,
|
||||
isOnboarding = false,
|
||||
}: AuthPageContainerProps): JSX.Element {
|
||||
return (
|
||||
<div className="auth-page-wrapper">
|
||||
<div className="auth-page-background">
|
||||
<div className="auth-page-dots bg-dot-pattern masked-dots" />
|
||||
<div className="auth-page-gradient" />
|
||||
<div className="auth-page-line-left" />
|
||||
<div className="auth-page-line-right" />
|
||||
</div>
|
||||
<div className="auth-page-layout">
|
||||
<AuthHeader />
|
||||
<main
|
||||
className={`auth-page-content ${isOnboarding ? 'onboarding-flow' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<AuthFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AuthPageContainer.defaultProps = {
|
||||
isOnboarding: false,
|
||||
};
|
||||
|
||||
export default AuthPageContainer;
|
||||
@@ -4,7 +4,7 @@ import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsAppli
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// dynamic step interval
|
||||
@@ -57,7 +57,7 @@ export const celeryAllStateWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
@@ -121,7 +121,7 @@ export const celeryRetryStateWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'count',
|
||||
@@ -181,7 +181,7 @@ export const celeryFailedStateWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
@@ -241,7 +241,7 @@ export const celerySuccessStateWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
@@ -288,7 +288,7 @@ export const celeryTasksByWorkerWidgetData = (
|
||||
limit: 10,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'rate',
|
||||
@@ -351,7 +351,7 @@ export const celeryErrorByWorkerWidgetData = (
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
{
|
||||
dataSource: 'traces',
|
||||
@@ -385,7 +385,7 @@ export const celeryErrorByWorkerWidgetData = (
|
||||
},
|
||||
],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
{
|
||||
queryName: 'F1',
|
||||
@@ -436,7 +436,7 @@ export const celeryLatencyByWorkerWidgetData = (
|
||||
limit: 10,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'p99',
|
||||
@@ -485,7 +485,7 @@ export const celeryActiveTasksWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'avg',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: 'latest',
|
||||
@@ -539,7 +539,7 @@ export const celeryTaskLatencyWidgetData = (
|
||||
},
|
||||
],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: getStepInterval(startTime, endTime),
|
||||
timeAggregation: type || 'p99',
|
||||
@@ -590,7 +590,7 @@ export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
|
||||
},
|
||||
],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
@@ -652,7 +652,7 @@ export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
|
||||
},
|
||||
],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
@@ -715,7 +715,7 @@ export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
|
||||
},
|
||||
],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
@@ -776,7 +776,7 @@ export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
|
||||
},
|
||||
],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
@@ -838,7 +838,7 @@ export const celeryTimeSeriesTablesWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'avg',
|
||||
@@ -877,7 +877,7 @@ export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'last',
|
||||
reduceTo: ReduceOperators.LAST,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count_distinct',
|
||||
@@ -926,7 +926,7 @@ export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'last',
|
||||
reduceTo: ReduceOperators.LAST,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count_distinct',
|
||||
@@ -975,7 +975,7 @@ export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'last',
|
||||
reduceTo: ReduceOperators.LAST,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count_distinct',
|
||||
@@ -1024,7 +1024,7 @@ export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'last',
|
||||
reduceTo: ReduceOperators.LAST,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count_distinct',
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-left: 30px;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
102
frontend/src/components/CustomTimePicker/CalendarContainer.tsx
Normal file
102
frontend/src/components/CustomTimePicker/CalendarContainer.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Calendar } from '@signozhq/calendar';
|
||||
import { Button } from 'antd';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { CalendarIcon, Check, X } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import { DateRange } from './CustomTimePickerPopoverContent';
|
||||
|
||||
function CalendarContainer({
|
||||
dateRange,
|
||||
onSelectDateRange,
|
||||
onCancel,
|
||||
onApply,
|
||||
}: {
|
||||
dateRange: DateRange;
|
||||
onSelectDateRange: (dateRange: DateRange) => void;
|
||||
onCancel: () => void;
|
||||
onApply: () => void;
|
||||
}): JSX.Element {
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// this is to override the default behavior of the shadcn calendar component
|
||||
// if a range is already selected, clicking on a date will reset selection and set the new date as the start date
|
||||
const handleSelect = (
|
||||
_selected: DateRange | undefined,
|
||||
clickedDate?: Date,
|
||||
): void => {
|
||||
if (!clickedDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No dates selected → start new
|
||||
if (!dateRange?.from) {
|
||||
onSelectDateRange({ from: clickedDate });
|
||||
return;
|
||||
}
|
||||
|
||||
// Only start selected → complete the range
|
||||
if (dateRange.from && !dateRange.to) {
|
||||
if (clickedDate < dateRange.from) {
|
||||
onSelectDateRange({ from: clickedDate, to: dateRange.from });
|
||||
} else {
|
||||
onSelectDateRange({ from: dateRange.from, to: clickedDate });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectDateRange({ from: clickedDate, to: undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="calendar-container">
|
||||
<div className="calendar-container-header">
|
||||
<CalendarIcon size={12} />
|
||||
<div className="calendar-container-header-title">
|
||||
{dayjs(dateRange?.from)
|
||||
.tz(timezone.value)
|
||||
.format(DATE_TIME_FORMATS.MONTH_DATE_SHORT)}{' '}
|
||||
-{' '}
|
||||
{dayjs(dateRange?.to)
|
||||
.tz(timezone.value)
|
||||
.format(DATE_TIME_FORMATS.MONTH_DATE_SHORT)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="calendar-container-body">
|
||||
<Calendar
|
||||
mode="range"
|
||||
required
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
disabled={{
|
||||
after: dayjs().toDate(),
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
|
||||
<div className="calendar-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn secondary cancel-btn"
|
||||
onClick={onCancel}
|
||||
icon={<X size={12} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn primary apply-btn"
|
||||
onClick={onApply}
|
||||
icon={<Check size={12} />}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalendarContainer;
|
||||
@@ -36,7 +36,6 @@
|
||||
}
|
||||
|
||||
.time-selection-dropdown-content {
|
||||
min-width: 172px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -48,18 +47,16 @@
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
|
||||
&.custom-time {
|
||||
input:not(:focus) {
|
||||
min-width: 280px;
|
||||
input {
|
||||
width: 280px;
|
||||
|
||||
&::placeholder {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input:focus::placeholder {
|
||||
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||
&:focus::placeholder {
|
||||
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +172,26 @@
|
||||
}
|
||||
|
||||
.time-input-prefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
width: 36px;
|
||||
|
||||
font-size: 11px;
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: var(--bg-ink-200);
|
||||
|
||||
&.is-live {
|
||||
background-color: transparent;
|
||||
color: var(--bg-forest-500);
|
||||
}
|
||||
|
||||
.live-dot-icon {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-forest-500);
|
||||
animation: ripple 1s infinite;
|
||||
@@ -191,7 +205,7 @@
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
70% {
|
||||
60% {
|
||||
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
|
||||
}
|
||||
100% {
|
||||
@@ -251,6 +265,11 @@
|
||||
background: rgb(179 179 179 / 15%);
|
||||
}
|
||||
|
||||
.time-input-prefix {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.time-input-suffix-icon-badge {
|
||||
color: var(--bg-ink-100);
|
||||
background: rgb(179 179 179 / 15%);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import './CustomTimePicker.styles.scss';
|
||||
|
||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||
import { Input, InputRef, Popover, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
@@ -13,10 +14,9 @@ import {
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { isValidTimeFormat } from 'lib/getMinMax';
|
||||
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
import { defaultTo, isFunction, noop } from 'lodash-es';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
ChangeEvent,
|
||||
@@ -25,20 +25,26 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeDifference, validateEpochRange } from 'utils/epochUtils';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { TimeRangeValidationResult, validateTimeRange } from 'utils/timeUtils';
|
||||
|
||||
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||
|
||||
const maxAllowedMinTimeInMonths = 6;
|
||||
const maxAllowedMinTimeInMonths = 15;
|
||||
type ViewType = 'datetime' | 'timezone';
|
||||
const DEFAULT_VIEW: ViewType = 'datetime';
|
||||
|
||||
export enum CustomTimePickerInputStatus {
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
UNSET = '',
|
||||
}
|
||||
|
||||
interface CustomTimePickerProps {
|
||||
onSelect: (value: string) => void;
|
||||
onError: (value: boolean) => void;
|
||||
@@ -64,6 +70,8 @@ interface CustomTimePickerProps {
|
||||
onExitLiveLogs?: () => void;
|
||||
/** When false, hides the "Recently Used" time ranges section */
|
||||
showRecentlyUsed?: boolean;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
}
|
||||
|
||||
function CustomTimePicker({
|
||||
@@ -84,23 +92,24 @@ function CustomTimePicker({
|
||||
onExitLiveLogs,
|
||||
showLiveLogs,
|
||||
showRecentlyUsed = true,
|
||||
minTime,
|
||||
maxTime,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
setSelectedTimePlaceholderValue,
|
||||
] = useState('Select / Enter Time Range');
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||
null,
|
||||
const [inputStatus, setInputStatus] = useState<CustomTimePickerInputStatus>(
|
||||
CustomTimePickerInputStatus.UNSET,
|
||||
);
|
||||
const [inputErrorDetails, setInputErrorDetails] = useState<
|
||||
TimeRangeValidationResult['errorDetails'] | null
|
||||
>(null);
|
||||
const location = useLocation();
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
|
||||
|
||||
@@ -123,12 +132,48 @@ function CustomTimePicker({
|
||||
|
||||
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
|
||||
|
||||
// function to get selected time in Last 1m, Last 2h, Last 3d, Last 4w format
|
||||
// 1m, 2h, 3d, 4w -> Last 1 minute, Last 2 hours, Last 3 days, Last 4 weeks
|
||||
const getSelectedTimeRangeLabelInRelativeFormat = (
|
||||
selectedTime: string,
|
||||
): string => {
|
||||
if (!selectedTime || selectedTime === 'custom') {
|
||||
return selectedTime || '';
|
||||
}
|
||||
|
||||
// Check if the format matches the relative time format (e.g., 1m, 2h, 3d, 4w)
|
||||
const match = selectedTime.match(/^(\d+)([mhdw])$/);
|
||||
if (!match) {
|
||||
// If it doesn't match the format, return as is
|
||||
return `Last ${selectedTime}`;
|
||||
}
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
|
||||
// Map unit abbreviations to full words
|
||||
const unitMap: Record<string, { singular: string; plural: string }> = {
|
||||
m: { singular: 'minute', plural: 'minutes' },
|
||||
h: { singular: 'hour', plural: 'hours' },
|
||||
d: { singular: 'day', plural: 'days' },
|
||||
w: { singular: 'week', plural: 'weeks' },
|
||||
};
|
||||
|
||||
const unitLabel = value === 1 ? unitMap[unit].singular : unitMap[unit].plural;
|
||||
|
||||
return `Last ${value} ${unitLabel}`;
|
||||
};
|
||||
|
||||
const getSelectedTimeRangeLabel = (
|
||||
selectedTime: string,
|
||||
selectedTimeValue: string,
|
||||
): string => {
|
||||
if (!selectedTime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (selectedTime === 'custom') {
|
||||
// TODO(shaheer): if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
|
||||
// TODO: if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
|
||||
// // Convert the date range string to 12-hour format
|
||||
// const dates = selectedTimeValue.split(' - ');
|
||||
// if (dates.length === 2) {
|
||||
@@ -164,42 +209,90 @@ function CustomTimePicker({
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidTimeFormat(selectedTime)) {
|
||||
return selectedTime;
|
||||
if (isValidShortHandDateTimeFormat(selectedTime)) {
|
||||
return getSelectedTimeRangeLabelInRelativeFormat(selectedTime);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const resetErrorStatus = (): void => {
|
||||
setInputStatus(CustomTimePickerInputStatus.UNSET);
|
||||
onError(false);
|
||||
setInputErrorDetails(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
setInputValue('Live');
|
||||
resetErrorStatus();
|
||||
} else {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
setInputValue(value);
|
||||
resetErrorStatus();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
||||
|
||||
const hide = (): void => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const getInputPrefix = (): JSX.Element => {
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<span className="time-input-prefix is-live">
|
||||
<span className="live-dot-icon" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const timeDifference = getTimeDifference(
|
||||
Number(minTime / 1000_000),
|
||||
Number(maxTime / 1000_000),
|
||||
);
|
||||
|
||||
return <span className="time-input-prefix">{timeDifference}</span>;
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean): void => {
|
||||
setOpen(newOpen);
|
||||
|
||||
if (!newOpen) {
|
||||
setCustomDTPickerVisible?.(false);
|
||||
setActiveView('datetime');
|
||||
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
setInputValue('Live');
|
||||
return;
|
||||
}
|
||||
|
||||
// set the input value to a relative format if the selected time is not custom
|
||||
const inputValue = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setInputValue(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleInputChange = debounce((inputValue): void => {
|
||||
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||
if (isValidFormat) {
|
||||
setInputStatus('success');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const inputValue = event.target.value;
|
||||
setInputValue(inputValue);
|
||||
|
||||
const match = inputValue.match(/^(\d+)([mhdw])$/);
|
||||
resetErrorStatus();
|
||||
};
|
||||
|
||||
const handleInputPressEnter = (): void => {
|
||||
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
||||
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||
|
||||
if (isTimeDurationShortHandFormat) {
|
||||
setInputStatus(CustomTimePickerInputStatus.SUCCESS);
|
||||
onError(false);
|
||||
setInputErrorDetails(null);
|
||||
|
||||
const match = inputValue.match(/^(\d+)([mhdw])$/) as RegExpMatchArray;
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
@@ -230,9 +323,13 @@ function CustomTimePicker({
|
||||
}
|
||||
|
||||
if (minTime && (!minTime.isValid() || minTime < maxAllowedMinTime)) {
|
||||
setInputStatus('error');
|
||||
setInputStatus(CustomTimePickerInputStatus.ERROR);
|
||||
onError(true);
|
||||
setInputErrorMessage('Please enter time less than 6 months');
|
||||
setInputErrorDetails({
|
||||
message: `Please enter time less than ${maxAllowedMinTimeInMonths} months`,
|
||||
code: 'TIME_LESS_THAN_MAX_ALLOWED_TIME_IN_MONTHS',
|
||||
description: `Please enter time less than ${maxAllowedMinTimeInMonths} months`,
|
||||
});
|
||||
if (isFunction(onCustomTimeStatusUpdate)) {
|
||||
onCustomTimeStatusUpdate(true);
|
||||
}
|
||||
@@ -241,44 +338,64 @@ function CustomTimePicker({
|
||||
time: [minTime, currentTime],
|
||||
timeStr: inputValue,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
}
|
||||
} else {
|
||||
setInputStatus('error');
|
||||
onError(true);
|
||||
setInputErrorMessage(null);
|
||||
if (isFunction(onCustomTimeStatusUpdate)) {
|
||||
onCustomTimeStatusUpdate(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const inputValue = event.target.value;
|
||||
// parse the input value to get the start and end time
|
||||
const [startTime, endTime] = inputValue.split(/\s[-–]\s/);
|
||||
|
||||
// check if startTime and endTime are epoch format
|
||||
const { isValid: isValidStartTime, range: epochRange } = validateEpochRange(
|
||||
Number(startTime),
|
||||
Number(endTime),
|
||||
);
|
||||
|
||||
if (isValidStartTime && epochRange?.startTime && epochRange?.endTime) {
|
||||
onCustomDateHandler?.([epochRange?.startTime, epochRange?.endTime]);
|
||||
|
||||
if (inputValue.length > 0) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
setOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValue(inputValue);
|
||||
const {
|
||||
isValid: isValidTimeRange,
|
||||
errorDetails,
|
||||
startTimeMs,
|
||||
endTimeMs,
|
||||
} = validateTimeRange(
|
||||
startTime,
|
||||
endTime,
|
||||
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
|
||||
);
|
||||
|
||||
// Call the debounced function with the input value
|
||||
debouncedHandleInputChange(inputValue);
|
||||
if (!isValidTimeRange) {
|
||||
setInputStatus(CustomTimePickerInputStatus.ERROR);
|
||||
onError(true);
|
||||
setInputErrorDetails(errorDetails || null);
|
||||
return;
|
||||
}
|
||||
|
||||
onCustomDateHandler?.([dayjs(startTimeMs), dayjs(endTimeMs)]);
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSelect = (label: string, value: string): void => {
|
||||
if (label === 'Custom') {
|
||||
if (value === 'custom') {
|
||||
setCustomDTPickerVisible?.(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(value);
|
||||
setSelectedTimePlaceholderValue(label);
|
||||
setInputStatus('');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
resetErrorStatus();
|
||||
setInputValue('');
|
||||
|
||||
if (value !== 'custom') {
|
||||
hide();
|
||||
}
|
||||
@@ -305,20 +422,48 @@ function CustomTimePicker({
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleFocus = (): void => {
|
||||
setIsInputFocused(true);
|
||||
setActiveView('datetime');
|
||||
const handleOpen = (e: React.SyntheticEvent): void => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (showLiveLogs) {
|
||||
setOpen(true);
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
setInputValue('Live');
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
// reset the input status and error message as we reset the time to previous correct value
|
||||
resetErrorStatus();
|
||||
|
||||
const startTime = dayjs(minTime / 1000_000).format(
|
||||
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
|
||||
);
|
||||
const endTime = dayjs(maxTime / 1000_000).format(
|
||||
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
|
||||
);
|
||||
|
||||
setInputValue(`${startTime} - ${endTime}`);
|
||||
};
|
||||
|
||||
const handleBlur = (): void => {
|
||||
setIsInputFocused(false);
|
||||
const handleClose = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
setCustomDTPickerVisible?.(false);
|
||||
|
||||
if (showLiveLogs) {
|
||||
setInputValue('Live');
|
||||
return;
|
||||
}
|
||||
|
||||
// set the input value to a relative format if the selected time is not custom
|
||||
const inputValue = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setInputValue(inputValue);
|
||||
};
|
||||
|
||||
// this is required as TopNav component wraps the components and we need to clear the state on path change
|
||||
useEffect(() => {
|
||||
setInputStatus('');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
resetErrorStatus();
|
||||
setInputValue('');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname]);
|
||||
@@ -335,6 +480,10 @@ function CustomTimePicker({
|
||||
);
|
||||
};
|
||||
|
||||
const handleInputBlur = (): void => {
|
||||
resetErrorStatus();
|
||||
};
|
||||
|
||||
const getTooltipTitle = (): string => {
|
||||
if (selectedTime === 'custom' && inputValue === '' && !open) {
|
||||
return `${dayjs(minTime / 1000_000)
|
||||
@@ -349,27 +498,19 @@ function CustomTimePicker({
|
||||
return '';
|
||||
};
|
||||
|
||||
const getInputPrefix = (): JSX.Element => {
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<div className="time-input-prefix">
|
||||
<div className="live-dot-icon" />
|
||||
</div>
|
||||
);
|
||||
// Focus and select input text when popover opens
|
||||
useEffect(() => {
|
||||
if (open && inputRef.current) {
|
||||
// Use setTimeout to wait for React to update the DOM and make input editable
|
||||
setTimeout(() => {
|
||||
const inputElement = inputRef.current?.input;
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
inputElement.select();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="time-input-prefix">
|
||||
{inputValue && inputStatus === 'success' ? (
|
||||
<CheckCircle size={14} color="#51E7A8" />
|
||||
) : (
|
||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||
<Clock size={14} className="cursor-pointer" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="custom-time-picker">
|
||||
@@ -385,9 +526,10 @@ function CustomTimePicker({
|
||||
content={
|
||||
newPopover ? (
|
||||
<CustomTimePickerPopoverContent
|
||||
isLiveLogsEnabled={!!showLiveLogs}
|
||||
setIsOpen={setOpen}
|
||||
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||
onSelectHandler={handleSelect}
|
||||
onGoLive={defaultTo(onGoLive, noop)}
|
||||
@@ -399,6 +541,10 @@ function CustomTimePicker({
|
||||
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||
isOpenedFromFooter={isOpenedFromFooter}
|
||||
showRecentlyUsed={showRecentlyUsed}
|
||||
customDateTimeInputStatus={inputStatus}
|
||||
inputErrorDetails={inputErrorDetails}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
) : (
|
||||
content
|
||||
@@ -407,25 +553,32 @@ function CustomTimePicker({
|
||||
arrow={false}
|
||||
trigger="click"
|
||||
open={open}
|
||||
destroyTooltipOnHide
|
||||
onOpenChange={handleOpenChange}
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
className="timeSelection-input"
|
||||
ref={inputRef}
|
||||
className={cx(
|
||||
'timeSelection-input',
|
||||
inputStatus === CustomTimePickerInputStatus.ERROR ? 'error' : '',
|
||||
)}
|
||||
type="text"
|
||||
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
||||
placeholder={
|
||||
isInputFocused
|
||||
? 'Time Format (1m or 2h or 3d or 4w)'
|
||||
: selectedTimePlaceholderValue
|
||||
status={
|
||||
inputValue && inputStatus === CustomTimePickerInputStatus.ERROR
|
||||
? 'error'
|
||||
: ''
|
||||
}
|
||||
readOnly={!open || showLiveLogs}
|
||||
placeholder={selectedTimePlaceholderValue}
|
||||
value={inputValue}
|
||||
onFocus={handleFocus}
|
||||
onClick={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleOpen}
|
||||
onClick={handleOpen}
|
||||
onChange={handleInputChange}
|
||||
onPressEnter={handleInputPressEnter}
|
||||
onBlur={handleInputBlur}
|
||||
data-1p-ignore
|
||||
prefix={getInputPrefix()}
|
||||
suffix={
|
||||
@@ -435,24 +588,25 @@ function CustomTimePicker({
|
||||
<span>{activeTimezoneOffset}</span>
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className="cursor-pointer time-input-suffix-icon-badge"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
handleViewChange('datetime');
|
||||
}}
|
||||
/>
|
||||
|
||||
{open ? (
|
||||
<ChevronUp
|
||||
size={14}
|
||||
className="cursor-pointer time-input-suffix-icon-badge"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className="cursor-pointer time-input-suffix-icon-badge"
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
{inputStatus === 'error' && inputErrorMessage && (
|
||||
<Typography.Title level={5} className="valid-format-error">
|
||||
{inputErrorMessage}
|
||||
</Typography.Title>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { Clock, PenLine } from 'lucide-react';
|
||||
import { Clock, PenLine, TriangleAlertIcon } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
Dispatch,
|
||||
@@ -27,10 +26,23 @@ import {
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
||||
import { TimeRangeValidationResult } from 'utils/timeUtils';
|
||||
|
||||
import CalendarContainer from './CalendarContainer';
|
||||
import { CustomTimePickerInputStatus } from './CustomTimePicker';
|
||||
import TimezonePicker from './TimezonePicker';
|
||||
|
||||
const TO_MILLISECONDS_FACTOR = 1000_000;
|
||||
|
||||
export type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
interface CustomTimePickerPopoverContentProps {
|
||||
isLiveLogsEnabled: boolean;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
options: any[];
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
customDateTimeVisible: boolean;
|
||||
@@ -48,6 +60,8 @@ interface CustomTimePickerPopoverContentProps {
|
||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||
onExitLiveLogs: () => void;
|
||||
showRecentlyUsed: boolean;
|
||||
customDateTimeInputStatus: CustomTimePickerInputStatus;
|
||||
inputErrorDetails: TimeRangeValidationResult['errorDetails'] | null;
|
||||
}
|
||||
|
||||
interface RecentlyUsedDateTimeRange {
|
||||
@@ -58,8 +72,29 @@ interface RecentlyUsedDateTimeRange {
|
||||
to: string;
|
||||
}
|
||||
|
||||
const getDateRange = (
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
timezone: string,
|
||||
): DateRange => {
|
||||
const from = dayjs(minTime / TO_MILLISECONDS_FACTOR)
|
||||
.tz(timezone)
|
||||
.startOf('day')
|
||||
.toDate();
|
||||
|
||||
const to = dayjs(maxTime / TO_MILLISECONDS_FACTOR)
|
||||
.tz(timezone)
|
||||
.endOf('day')
|
||||
.toDate();
|
||||
|
||||
return { from, to };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function CustomTimePickerPopoverContent({
|
||||
isLiveLogsEnabled,
|
||||
minTime,
|
||||
maxTime,
|
||||
options,
|
||||
setIsOpen,
|
||||
customDateTimeVisible,
|
||||
@@ -74,6 +109,8 @@ function CustomTimePickerPopoverContent({
|
||||
setIsOpenedFromFooter,
|
||||
onExitLiveLogs,
|
||||
showRecentlyUsed = true,
|
||||
customDateTimeInputStatus = CustomTimePickerInputStatus.UNSET,
|
||||
inputErrorDetails,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -83,6 +120,9 @@ function CustomTimePickerPopoverContent({
|
||||
|
||||
const url = new URLSearchParams(window.location.search);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const activeTimezoneOffset = timezone.offset;
|
||||
|
||||
let panelTypeFromURL = url.get(QueryParams.panelTypes);
|
||||
|
||||
try {
|
||||
@@ -94,8 +134,9 @@ function CustomTimePickerPopoverContent({
|
||||
const isLogsListView =
|
||||
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const activeTimezoneOffset = timezone.offset;
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
getDateRange(minTime, maxTime, timezone.value),
|
||||
);
|
||||
|
||||
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
|
||||
RecentlyUsedDateTimeRange[]
|
||||
@@ -177,36 +218,66 @@ function CustomTimePickerPopoverContent({
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleSelectDateRange = (dateRange: DateRange): void => {
|
||||
setDateRange(dateRange);
|
||||
};
|
||||
|
||||
const handleCalendarRangeApply = (): void => {
|
||||
if (dateRange) {
|
||||
const from = dayjs(dateRange.from)
|
||||
.tz(timezone.value)
|
||||
.startOf('day')
|
||||
.toDate();
|
||||
const to = dayjs(dateRange.to).tz(timezone.value).endOf('day').toDate();
|
||||
|
||||
onCustomDateHandler([dayjs(from), dayjs(to)]);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleCalendarRangeCancel = (): void => {
|
||||
setCustomDTPickerVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="date-time-popover">
|
||||
{!customDateTimeVisible && (
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||
Live
|
||||
</Button>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-options-btn',
|
||||
customDateTimeVisible
|
||||
? option.value === 'custom' && 'active'
|
||||
: selectedTime === option.value && 'active',
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
<Button
|
||||
className={cx('data-time-live', isLiveLogsEnabled ? 'active' : '')}
|
||||
type="text"
|
||||
onClick={handleGoLive}
|
||||
>
|
||||
Live
|
||||
</Button>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-options-btn',
|
||||
customDateTimeVisible
|
||||
? option.value === 'custom' && !isLiveLogsEnabled && 'active'
|
||||
: selectedTime === option.value && !isLiveLogsEnabled && 'active',
|
||||
)}
|
||||
>
|
||||
<span className="time-label">{option.label}</span>
|
||||
|
||||
{option.value !== 'custom' && option.value !== '1month' && (
|
||||
<span className="time-value">{option.value}</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'relative-date-time',
|
||||
@@ -214,19 +285,38 @@ function CustomTimePickerPopoverContent({
|
||||
)}
|
||||
>
|
||||
{customDateTimeVisible ? (
|
||||
<DatePickerV2
|
||||
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||
setIsOpen={setIsOpen}
|
||||
onCustomDateHandler={onCustomDateHandler}
|
||||
<CalendarContainer
|
||||
dateRange={dateRange}
|
||||
onSelectDateRange={handleSelectDateRange}
|
||||
onCancel={handleCalendarRangeCancel}
|
||||
onApply={handleCalendarRangeApply}
|
||||
/>
|
||||
) : (
|
||||
<div className="time-selector-container">
|
||||
{customDateTimeInputStatus === CustomTimePickerInputStatus.ERROR &&
|
||||
inputErrorDetails && (
|
||||
<div className="input-error-message-container">
|
||||
<div className="input-error-message-title">
|
||||
<TriangleAlertIcon color={Color.BG_CHERRY_400} size={16} />
|
||||
<span className="input-error-message-text">
|
||||
{inputErrorDetails.message}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{inputErrorDetails.description && (
|
||||
<p className="input-error-message-description">
|
||||
{inputErrorDetails.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative-times-container">
|
||||
<div className="time-heading">RELATIVE TIMES</div>
|
||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||
</div>
|
||||
|
||||
{showRecentlyUsed && (
|
||||
{showRecentlyUsed && recentlyUsedTimeRanges.length > 0 && (
|
||||
<div className="recently-used-container">
|
||||
<div className="time-heading">RECENTLY USED</div>
|
||||
<div className="recently-used-range">
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
.date-picker-v2-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.custom-date-time-picker-v2 {
|
||||
padding: 12px;
|
||||
|
||||
.periscope-calendar {
|
||||
border-radius: 4px;
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
padding: 8px 0 !important;
|
||||
}
|
||||
|
||||
.periscope-calendar-day {
|
||||
background: none !important;
|
||||
|
||||
&.periscope-calendar-today {
|
||||
&.text-accent-foreground {
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-time-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.time-input {
|
||||
border-radius: 4px;
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
padding: 8px 4px !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-date-time-picker-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
|
||||
.next-btn {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invalid-date-range-tooltip {
|
||||
.ant-tooltip-inner {
|
||||
color: var(--bg-sakura-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.custom-date-time-picker-v2 {
|
||||
.periscope-calendar-day {
|
||||
&.periscope-calendar-today {
|
||||
&.text-accent-foreground {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-time-selector {
|
||||
.time-input {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
import './DatePickerV2.styles.scss';
|
||||
|
||||
import { Calendar } from '@signozhq/calendar';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { CornerUpLeft, MoveRight } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { addCustomTimeRange } from 'utils/customTimeRangeUtils';
|
||||
|
||||
function DatePickerV2({
|
||||
onSetCustomDTPickerVisible,
|
||||
setIsOpen,
|
||||
onCustomDateHandler,
|
||||
}: {
|
||||
onSetCustomDTPickerVisible: (visible: boolean) => void;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
onCustomDateHandler: (
|
||||
dateTimeRange: DateTimeRangeType,
|
||||
lexicalContext?: LexicalContext,
|
||||
) => void;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const timeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const [selectedDateTimeFor, setSelectedDateTimeFor] = useState<'to' | 'from'>(
|
||||
'from',
|
||||
);
|
||||
|
||||
const [selectedFromDateTime, setSelectedFromDateTime] = useState<Dayjs | null>(
|
||||
dayjs(minTime / 1000_000).tz(timezone.value),
|
||||
);
|
||||
|
||||
const [selectedToDateTime, setSelectedToDateTime] = useState<Dayjs | null>(
|
||||
dayjs(maxTime / 1000_000).tz(timezone.value),
|
||||
);
|
||||
|
||||
const handleNext = (): void => {
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
|
||||
|
||||
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
|
||||
|
||||
setIsOpen(false);
|
||||
onSetCustomDTPickerVisible(false);
|
||||
setSelectedDateTimeFor('from');
|
||||
} else {
|
||||
setSelectedDateTimeFor('to');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateChange = (date: Date | undefined): void => {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
const prevFromDateTime = selectedFromDateTime;
|
||||
|
||||
const newDate = dayjs(date);
|
||||
|
||||
const updatedFromDateTime = prevFromDateTime
|
||||
? prevFromDateTime
|
||||
.year(newDate.year())
|
||||
.month(newDate.month())
|
||||
.date(newDate.date())
|
||||
: dayjs(date).tz(timezone.value);
|
||||
|
||||
setSelectedFromDateTime(updatedFromDateTime);
|
||||
} else {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSelectedToDateTime((prev) => {
|
||||
const newDate = dayjs(date);
|
||||
|
||||
// Update only the date part, keeping time from existing state
|
||||
return prev
|
||||
? prev.year(newDate.year()).month(newDate.month()).date(newDate.date())
|
||||
: dayjs(date).tz(timezone.value);
|
||||
});
|
||||
}
|
||||
|
||||
// focus the time input
|
||||
timeInputRef?.current?.focus();
|
||||
};
|
||||
|
||||
const handleTimeChange = (time: string): void => {
|
||||
// time should have format HH:mm:ss
|
||||
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
setSelectedFromDateTime((prev) => {
|
||||
if (prev) {
|
||||
return prev
|
||||
.set('hour', parseInt(time.split(':')[0], 10))
|
||||
.set('minute', parseInt(time.split(':')[1], 10))
|
||||
.set('second', parseInt(time.split(':')[2], 10));
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSelectedToDateTime((prev) => {
|
||||
if (prev) {
|
||||
return prev
|
||||
.set('hour', parseInt(time.split(':')[0], 10))
|
||||
.set('minute', parseInt(time.split(':')[1], 10))
|
||||
.set('second', parseInt(time.split(':')[2], 10));
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultMonth = (): Date => {
|
||||
let defaultDate = null;
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
defaultDate = selectedFromDateTime?.toDate();
|
||||
} else if (selectedDateTimeFor === 'to') {
|
||||
defaultDate = selectedToDateTime?.toDate();
|
||||
}
|
||||
|
||||
return defaultDate ?? new Date();
|
||||
};
|
||||
|
||||
const isValidRange = (): boolean => {
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
return selectedToDateTime?.isAfter(selectedFromDateTime) ?? false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleBack = (): void => {
|
||||
setSelectedDateTimeFor('from');
|
||||
};
|
||||
|
||||
const handleHideCustomDTPicker = (): void => {
|
||||
onSetCustomDTPickerVisible(false);
|
||||
};
|
||||
|
||||
const handleSelectDateTimeFor = (selectedDateTimeFor: 'to' | 'from'): void => {
|
||||
setSelectedDateTimeFor(selectedDateTimeFor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="date-picker-v2-container">
|
||||
<div className="date-time-custom-options-container">
|
||||
<div
|
||||
className="back-btn"
|
||||
onClick={handleHideCustomDTPicker}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleHideCustomDTPicker();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CornerUpLeft size={16} />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
|
||||
<div className="date-time-custom-options">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSelectDateTimeFor('from');
|
||||
}
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-custom-option-from',
|
||||
selectedDateTimeFor === 'from' && 'active',
|
||||
)}
|
||||
onClick={(): void => {
|
||||
handleSelectDateTimeFor('from');
|
||||
}}
|
||||
>
|
||||
<div className="date-time-custom-option-from-title">FROM</div>
|
||||
<div className="date-time-custom-option-from-value">
|
||||
{selectedFromDateTime?.format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSelectDateTimeFor('to');
|
||||
}
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-custom-option-to',
|
||||
selectedDateTimeFor === 'to' && 'active',
|
||||
)}
|
||||
onClick={(): void => {
|
||||
handleSelectDateTimeFor('to');
|
||||
}}
|
||||
>
|
||||
<div className="date-time-custom-option-to-title">TO</div>
|
||||
<div className="date-time-custom-option-to-value">
|
||||
{selectedToDateTime?.format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-date-time-picker-v2">
|
||||
<Calendar
|
||||
mode="single"
|
||||
required
|
||||
selected={
|
||||
selectedDateTimeFor === 'from'
|
||||
? selectedFromDateTime?.toDate()
|
||||
: selectedToDateTime?.toDate()
|
||||
}
|
||||
key={selectedDateTimeFor + selectedDateTimeFor}
|
||||
onSelect={handleDateChange}
|
||||
defaultMonth={getDefaultMonth()}
|
||||
disabled={(current): boolean => {
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
// disable dates after today and before selectedFromDateTime
|
||||
const currentDay = dayjs(current);
|
||||
return currentDay.isAfter(dayjs()) || false;
|
||||
}
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
// disable dates after selectedToDateTime
|
||||
|
||||
return dayjs(current).isAfter(dayjs()) || false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}}
|
||||
className="rounded-md border"
|
||||
navLayout="after"
|
||||
/>
|
||||
|
||||
<div className="custom-time-selector">
|
||||
<label className="text-xs font-normal block" htmlFor="time-picker">
|
||||
Timestamp
|
||||
</label>
|
||||
|
||||
<MoveRight size={16} />
|
||||
|
||||
<div className="time-input-container">
|
||||
<Input
|
||||
type="time"
|
||||
ref={timeInputRef}
|
||||
className="time-input"
|
||||
value={
|
||||
selectedDateTimeFor === 'from'
|
||||
? selectedFromDateTime?.format('HH:mm:ss')
|
||||
: selectedToDateTime?.format('HH:mm:ss')
|
||||
}
|
||||
onChange={(e): void => handleTimeChange(e.target.value)}
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="custom-date-time-picker-footer">
|
||||
{selectedDateTimeFor === 'to' && (
|
||||
<Button
|
||||
className="periscope-btn secondary clear-btn"
|
||||
type="default"
|
||||
onClick={handleBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
title={
|
||||
!isValidRange() ? 'Invalid range: TO date should be after FROM date' : ''
|
||||
}
|
||||
overlayClassName="invalid-date-range-tooltip"
|
||||
>
|
||||
<Button
|
||||
className="periscope-btn primary next-btn"
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!isValidRange()}
|
||||
>
|
||||
{selectedDateTimeFor === 'from' ? 'Next' : 'Apply'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DatePickerV2;
|
||||
@@ -6,13 +6,15 @@ import ErrorIcon from 'assets/Error';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { BookOpenText, ChevronsDown } from 'lucide-react';
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { ReactNode } from 'react';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
interface ErrorContentProps {
|
||||
error: APIError;
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
||||
function ErrorContent({ error, icon }: ErrorContentProps): JSX.Element {
|
||||
const {
|
||||
url: errorUrl,
|
||||
errors: errorMessages,
|
||||
@@ -25,9 +27,7 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
||||
<section className="error-content__summary-section">
|
||||
<header className="error-content__summary">
|
||||
<div className="error-content__summary-left">
|
||||
<div className="error-content__icon-wrapper">
|
||||
<ErrorIcon />
|
||||
</div>
|
||||
<div className="error-content__icon-wrapper">{icon || <ErrorIcon />}</div>
|
||||
|
||||
<div className="error-content__summary-text">
|
||||
<h2 className="error-content__error-code">{errorCode}</h2>
|
||||
@@ -95,4 +95,8 @@ function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
ErrorContent.defaultProps = {
|
||||
icon: undefined,
|
||||
};
|
||||
|
||||
export default ErrorContent;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
||||
// eslint-disable-next-line import/namespace -- side-effect import that registers Chart.js date adapter
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
|
||||
@@ -176,6 +176,8 @@ function HostMetricTraces({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { nanoToMilli } from 'utils/timeUtils';
|
||||
|
||||
export const columns = [
|
||||
@@ -121,7 +121,7 @@ export const getHostTracesQueryPayload = (
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
|
||||
@@ -87,6 +87,8 @@ function HostMetricLogsDetailedView({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getHostLogsQueryPayload = (
|
||||
@@ -45,7 +45,7 @@ export const getHostLogsQueryPayload = (
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
offset: 0,
|
||||
pageSize: 100,
|
||||
},
|
||||
|
||||
@@ -211,6 +211,8 @@ function Metrics({
|
||||
defaultRelativeTime="5m"
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { DrawerProps } from 'antd';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { VIEWS } from './constants';
|
||||
|
||||
export type LogDetailProps = {
|
||||
log: ILog | null;
|
||||
selectedTab: VIEWS;
|
||||
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
isListViewPanel?: boolean;
|
||||
listViewPanelSelectedFields?: IField[] | null;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
|
||||
@@ -55,11 +55,11 @@ function LogDetailInner({
|
||||
log,
|
||||
onClose,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
onClickActionItem,
|
||||
selectedTab,
|
||||
isListViewPanel = false,
|
||||
listViewPanelSelectedFields,
|
||||
handleChangeSelectedView,
|
||||
}: LogDetailInnerProps): JSX.Element {
|
||||
const initialContextQuery = useInitialQuery(log);
|
||||
const [contextQuery, setContextQuery] = useState<Query | undefined>(
|
||||
@@ -365,10 +365,10 @@ function LogDetailInner({
|
||||
logData={log}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onClickActionItem}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
isListViewPanel={isListViewPanel}
|
||||
selectedOptions={options}
|
||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
||||
|
||||
@@ -6,6 +6,7 @@ import cx from 'classnames';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
@@ -108,6 +109,7 @@ type ListLogViewProps = {
|
||||
activeLog?: ILog | null;
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
};
|
||||
|
||||
function ListLogView({
|
||||
@@ -118,6 +120,7 @@ function ListLogView({
|
||||
activeLog,
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
handleChangeSelectedView,
|
||||
}: ListLogViewProps): JSX.Element {
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
|
||||
@@ -131,7 +134,6 @@ function ListLogView({
|
||||
onAddToQuery: handleAddToQuery,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -255,7 +257,7 @@ function ListLogView({
|
||||
onAddToQuery={handleAddToQuery}
|
||||
selectedTab={VIEW_TYPES.CONTEXT}
|
||||
onClose={handlerClearActiveContextLog}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -264,6 +266,7 @@ function ListLogView({
|
||||
|
||||
ListLogView.defaultProps = {
|
||||
activeLog: null,
|
||||
handleChangeSelectedView: undefined,
|
||||
};
|
||||
|
||||
LogGeneralField.defaultProps = {
|
||||
|
||||
@@ -39,6 +39,7 @@ function RawLogView({
|
||||
selectedFields = [],
|
||||
fontSize,
|
||||
onLogClick,
|
||||
handleChangeSelectedView,
|
||||
}: RawLogViewProps): JSX.Element {
|
||||
const {
|
||||
isHighlighted: isUrlHighlighted,
|
||||
@@ -52,7 +53,6 @@ function RawLogView({
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
onGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
|
||||
@@ -224,13 +224,12 @@ function RawLogView({
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
)}
|
||||
</RawLogViewContainer>
|
||||
);
|
||||
}
|
||||
|
||||
RawLogView.defaultProps = {
|
||||
isActiveLog: false,
|
||||
isReadOnly: false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { MouseEvent } from 'react';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
@@ -14,6 +15,7 @@ export interface RawLogViewProps {
|
||||
fontSize: FontSize;
|
||||
selectedFields?: IField[];
|
||||
onLogClick?: (log: ILog, event: MouseEvent) => void;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
}
|
||||
|
||||
export interface RawLogContentProps {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import VariableItem from '../../../container/NewDashboard/DashboardVariablesSelection/VariableItem';
|
||||
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
|
||||
|
||||
// Mock the dashboard variables query
|
||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
||||
import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
|
||||
import { OptionData } from './types';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import QueryAddOns from '../QueryV2/QueryAddOns/QueryAddOns';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
|
||||
// Mocks: only what is required for this component to render and for us to assert handler calls
|
||||
const mockHandleChangeQueryData = jest.fn();
|
||||
@@ -200,7 +200,7 @@ describe('QueryAddOns', () => {
|
||||
it('auto-opens reduce-to content when reduceTo is set', () => {
|
||||
render(
|
||||
<QueryAddOns
|
||||
query={baseQuery({ reduceTo: 'sum' })}
|
||||
query={baseQuery({ reduceTo: ReduceOperators.SUM })}
|
||||
version="v5"
|
||||
isListViewPanel={false}
|
||||
showReduceTo
|
||||
@@ -216,8 +216,10 @@ describe('QueryAddOns', () => {
|
||||
it('calls handleSetQueryData when reduce-to value changes', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const query = baseQuery({
|
||||
reduceTo: 'avg',
|
||||
aggregations: [{ id: 'a', operator: 'count', reduceTo: 'avg' }],
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
aggregations: [
|
||||
{ id: 'a', operator: 'count', reduceTo: ReduceOperators.AVG },
|
||||
],
|
||||
});
|
||||
render(
|
||||
<QueryAddOns
|
||||
@@ -258,7 +260,7 @@ describe('QueryAddOns', () => {
|
||||
aggregations: [
|
||||
{
|
||||
...(query.aggregations?.[0] as any),
|
||||
reduceTo: 'sum',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
@@ -803,7 +803,7 @@ describe('convertAggregationToExpression', () => {
|
||||
timeAggregation: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
alias: 'test_alias',
|
||||
reduceTo: 'sum',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
temporality: 'delta',
|
||||
});
|
||||
|
||||
@@ -812,7 +812,7 @@ describe('convertAggregationToExpression', () => {
|
||||
metricName: 'test_metric',
|
||||
timeAggregation: 'avg',
|
||||
spaceAggregation: 'max',
|
||||
reduceTo: 'sum',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
temporality: 'delta',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -34,7 +34,7 @@ function YAxisUnitSelector({
|
||||
initialValue,
|
||||
);
|
||||
const currentUniversalUnitName = getUniversalNameFromMetricUnit(value);
|
||||
return `Unit mismatch. Saved unit is ${initialUniversalUnitName}, but ${currentUniversalUnitName} is selected.`;
|
||||
return `Unit mismatch. The metric was sent with unit ${initialUniversalUnitName}, but ${currentUniversalUnitName} is selected.`;
|
||||
}
|
||||
return '';
|
||||
}, [initialValue, value, loading]);
|
||||
@@ -82,6 +82,7 @@ function YAxisUnitSelector({
|
||||
'warning-state': incompatibleUnitMessage,
|
||||
})}
|
||||
data-testid={dataTestId}
|
||||
allowClear
|
||||
>
|
||||
{categories.map((category) => (
|
||||
<Select.OptGroup key={category.name} label={category.name}>
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('YAxisUnitSelector', () => {
|
||||
fireEvent.mouseOver(warningIcon);
|
||||
return screen
|
||||
.findByText(
|
||||
'Unit mismatch. Saved unit is Seconds (s), but Bytes (B) is selected.',
|
||||
'Unit mismatch. The metric was sent with unit Seconds (s), but Bytes (B) is selected.',
|
||||
)
|
||||
.then((el) => expect(el).toBeInTheDocument());
|
||||
});
|
||||
|
||||
@@ -12,4 +12,8 @@
|
||||
.anticon {
|
||||
color: var(--bg-amber-400) !important;
|
||||
}
|
||||
|
||||
.ant-select-clear {
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,4 +51,7 @@ export enum QueryParams {
|
||||
thresholds = 'thresholds',
|
||||
selectedExplorerView = 'selectedExplorerView',
|
||||
variables = 'variables',
|
||||
version = 'version',
|
||||
showNewCreateAlertsPage = 'showNewCreateAlertsPage',
|
||||
source = 'source',
|
||||
}
|
||||
|
||||
@@ -138,11 +138,11 @@ export const mapOfFormulaToFilters: Record<
|
||||
};
|
||||
|
||||
export const REDUCE_TO_VALUES: SelectOption<ReduceOperators, string>[] = [
|
||||
{ value: 'last', label: 'Latest of values in timeframe' },
|
||||
{ value: 'sum', label: 'Sum of values in timeframe' },
|
||||
{ value: 'avg', label: 'Average of values in timeframe' },
|
||||
{ value: 'max', label: 'Max of values in timeframe' },
|
||||
{ value: 'min', label: 'Min of values in timeframe' },
|
||||
{ value: ReduceOperators.LAST, label: 'Latest of values in timeframe' },
|
||||
{ value: ReduceOperators.SUM, label: 'Sum of values in timeframe' },
|
||||
{ value: ReduceOperators.AVG, label: 'Average of values in timeframe' },
|
||||
{ value: ReduceOperators.MAX, label: 'Max of values in timeframe' },
|
||||
{ value: ReduceOperators.MIN, label: 'Min of values in timeframe' },
|
||||
];
|
||||
|
||||
export const initialHavingValues: HavingForm = {
|
||||
@@ -180,7 +180,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
temporality: '',
|
||||
timeAggregation: MetricAggregateOperator.COUNT,
|
||||
spaceAggregation: MetricAggregateOperator.SUM,
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
functions: [],
|
||||
@@ -196,7 +196,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
source: '',
|
||||
};
|
||||
|
||||
@@ -228,7 +228,7 @@ export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
|
||||
temporality: '',
|
||||
timeAggregation: MeterAggregateOperator.COUNT,
|
||||
spaceAggregation: MeterAggregateOperator.SUM,
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
functions: [],
|
||||
@@ -244,7 +244,7 @@ export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
};
|
||||
|
||||
export const initialQueryBuilderFormValuesMap: Record<
|
||||
@@ -301,6 +301,7 @@ export const initialQueryState: QueryState = {
|
||||
builder: initialQueryBuilderData,
|
||||
clickhouse_sql: [initialClickHouseData],
|
||||
promql: [initialQueryPromQLData],
|
||||
unit: '',
|
||||
};
|
||||
|
||||
const initialQueryWithType: Query = {
|
||||
|
||||
@@ -27,6 +27,7 @@ const ROUTES = {
|
||||
ALERTS_NEW: '/alerts/new',
|
||||
ALERT_HISTORY: '/alerts/history',
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
ALERT_TYPE_SELECTION: '/alerts/type-selection',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { domainNameKey } from './constants';
|
||||
@@ -401,7 +401,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
@@ -429,7 +429,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
@@ -457,7 +457,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
@@ -485,7 +485,7 @@ export const getDomainMetricsQueryPayload = (
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [
|
||||
@@ -653,7 +653,7 @@ export const getEndPointsQueryPayload = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -700,7 +700,7 @@ export const getEndPointsQueryPayload = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'p99',
|
||||
@@ -748,7 +748,7 @@ export const getEndPointsQueryPayload = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'max',
|
||||
@@ -805,7 +805,7 @@ export const getEndPointsQueryPayload = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'D',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -1431,7 +1431,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
@@ -1461,7 +1461,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'p99',
|
||||
@@ -1491,7 +1491,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -1521,7 +1521,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'D',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'max',
|
||||
@@ -1551,7 +1551,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'E',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -1629,7 +1629,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -1665,7 +1665,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'p99',
|
||||
@@ -1705,7 +1705,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
},
|
||||
],
|
||||
legend: 'rate',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
@@ -1781,7 +1781,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
type: 'attribute',
|
||||
},
|
||||
],
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
@@ -1850,7 +1850,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -1887,7 +1887,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'p99',
|
||||
@@ -1924,7 +1924,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
@@ -1961,7 +1961,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'D',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -2039,7 +2039,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
timeAggregation: 'rate',
|
||||
@@ -2110,7 +2110,7 @@ export const getEndPointDetailsQueryPayload = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
timeAggregation: 'p99',
|
||||
@@ -2208,7 +2208,7 @@ export const getEndPointZeroStateQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
@@ -2787,7 +2787,7 @@ export const getStatusCodeBarChartWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'rate',
|
||||
@@ -2909,7 +2909,7 @@ export const getAllEndpointsWidgetData = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -2941,7 +2941,7 @@ export const getAllEndpointsWidgetData = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'B',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'p99',
|
||||
@@ -2973,7 +2973,7 @@ export const getAllEndpointsWidgetData = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'max',
|
||||
@@ -3005,7 +3005,7 @@ export const getAllEndpointsWidgetData = (
|
||||
limit: 1000,
|
||||
orderBy: [],
|
||||
queryName: 'D',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count',
|
||||
@@ -3191,7 +3191,7 @@ export const getRateOverTimeWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
timeAggregation: 'rate',
|
||||
@@ -3242,7 +3242,7 @@ export const getLatencyOverTimeWidgetData = (
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'avg',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
timeAggregation: 'p99',
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as usePrefillAlertConditions from 'container/FormAlertRules/usePrefillAlertConditions';
|
||||
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
|
||||
import CreateAlertPage from 'pages/CreateAlert';
|
||||
import { act, fireEvent, render } from 'tests/test-utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(usePrefillAlertConditions, 'usePrefillAlertConditions')
|
||||
.mockReturnValue({
|
||||
@@ -66,13 +54,20 @@ describe('Alert rule documentation redirection', () => {
|
||||
window.open = mockWindowOpen;
|
||||
});
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERT_TYPE_SELECTION}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
renderResult = render(
|
||||
<CreateAlertPage />,
|
||||
<AlertTypeSelectionPage />,
|
||||
{},
|
||||
{
|
||||
initialRoute: ROUTES.ALERTS_NEW,
|
||||
initialRoute: ROUTES.ALERT_TYPE_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -117,18 +112,20 @@ describe('Alert rule documentation redirection', () => {
|
||||
|
||||
expect(mockWindowOpen).toHaveBeenCalledTimes(alertTypeCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create alert page redirection', () => {
|
||||
Object.values(AlertTypes)
|
||||
.filter((type) => type !== AlertTypes.ANOMALY_BASED_ALERT)
|
||||
.forEach((alertType) => {
|
||||
it(`should redirect to create alert page for ${alertType} and "Check an example alert" should redirect to the correct documentation`, () => {
|
||||
const { getByTestId, getByRole } = renderResult;
|
||||
|
||||
const alertTypeLink = getByTestId(`alert-type-card-${alertType}`);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(alertTypeLink);
|
||||
});
|
||||
const { getByRole } = render(
|
||||
<CreateAlertPage />,
|
||||
{},
|
||||
{
|
||||
initialRoute: `${ROUTES.ALERTS_NEW}?alertType=${alertType}`,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useCompositeQueryParamHooks from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import * as useUrlQueryHooks from 'hooks/useUrlQuery';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import CreateAlertRule from '../index';
|
||||
|
||||
jest.mock('container/FormAlertRules', () => ({
|
||||
__esModule: true,
|
||||
default: function MockFormAlertRules({
|
||||
alertType,
|
||||
}: {
|
||||
alertType: AlertTypes;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Form Alert Rules</h1>
|
||||
<p>{alertType}</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
AlertDetectionTypes: {
|
||||
THRESHOLD_ALERT: 'threshold_rule',
|
||||
ANOMALY_DETECTION_ALERT: 'anomaly_rule',
|
||||
},
|
||||
}));
|
||||
jest.mock('container/CreateAlertV2', () => ({
|
||||
__esModule: true,
|
||||
default: function MockCreateAlertV2(): JSX.Element {
|
||||
return <div>Create Alert V2</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
const useCompositeQueryParamSpy = jest.spyOn(
|
||||
useCompositeQueryParamHooks,
|
||||
'useGetCompositeQueryParam',
|
||||
);
|
||||
const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
|
||||
|
||||
const mockSetUrlQuery = jest.fn();
|
||||
const mockToString = jest.fn();
|
||||
const mockGetUrlQuery = jest.fn();
|
||||
|
||||
const FORM_ALERT_RULES_TEXT = 'Form Alert Rules';
|
||||
const CREATE_ALERT_V2_TEXT = 'Create Alert V2';
|
||||
|
||||
describe('CreateAlertRule', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useUrlQuerySpy.mockReturnValue(({
|
||||
set: mockSetUrlQuery,
|
||||
toString: mockToString,
|
||||
get: mockGetUrlQuery,
|
||||
} as Partial<URLSearchParams>) as URLSearchParams);
|
||||
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
|
||||
});
|
||||
|
||||
it('should render v1 flow when showNewCreateAlertsPage is false', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render v2 flow when showNewCreateAlertsPage is true', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showNewCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render v1 flow when ruleType is anomaly_rule even if showNewCreateAlertsPage is true', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showNewCreateAlertsPage) {
|
||||
return 'true';
|
||||
}
|
||||
if (key === QueryParams.ruleType) {
|
||||
return 'anomaly_rule';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from URL when provided', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.alertType) {
|
||||
return AlertTypes.LOGS_BASED_ALERT;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from compositeQuery dataSource when alertType is not in URL', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue({
|
||||
...initialQueriesMap.metrics,
|
||||
builder: {
|
||||
...initialQueriesMap.metrics.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.metrics.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should default to METRICS_BASED_ALERT when no alertType and no compositeQuery', () => {
|
||||
mockGetUrlQuery.mockReturnValue(null);
|
||||
useCompositeQueryParamSpy.mockReturnValue(null);
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,133 +1,49 @@
|
||||
import { Form, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Form } from 'antd';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import history from 'lib/history';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useMemo } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import {
|
||||
alertDefaults,
|
||||
anamolyAlertDefaults,
|
||||
exceptionAlertDefaults,
|
||||
logAlertDefaults,
|
||||
traceAlertDefaults,
|
||||
} from './defaults';
|
||||
import SelectAlertType from './SelectAlertType';
|
||||
import { ALERTS_VALUES_MAP } from './defaults';
|
||||
|
||||
function CreateRules(): JSX.Element {
|
||||
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const alertTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||
const version = queryParams.get('version');
|
||||
const alertTypeFromParams =
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
? AlertTypes.ANOMALY_BASED_ALERT
|
||||
: queryParams.get(QueryParams.alertType);
|
||||
|
||||
const { thresholds } = (location.state as {
|
||||
thresholds: ThresholdProps[];
|
||||
}) || {
|
||||
thresholds: null,
|
||||
};
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
function getAlertTypeFromDataSource(): AlertTypes | null {
|
||||
if (!compositeQuery) {
|
||||
return null;
|
||||
}
|
||||
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
||||
|
||||
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||
}
|
||||
|
||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||
(alertTypeFromParams as AlertTypes) || getAlertTypeFromDataSource(),
|
||||
);
|
||||
|
||||
const [formInstance] = Form.useForm();
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const queryParams = useUrlQuery();
|
||||
|
||||
const onSelectType = (typ: AlertTypes): void => {
|
||||
setAlertType(typ);
|
||||
|
||||
switch (typ) {
|
||||
case AlertTypes.LOGS_BASED_ALERT:
|
||||
setInitValues(logAlertDefaults);
|
||||
break;
|
||||
case AlertTypes.TRACES_BASED_ALERT:
|
||||
setInitValues(traceAlertDefaults);
|
||||
break;
|
||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||
setInitValues(exceptionAlertDefaults);
|
||||
break;
|
||||
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||
setInitValues({
|
||||
...anamolyAlertDefaults,
|
||||
version: version || ENTITY_VERSION_V5,
|
||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
setInitValues({
|
||||
...alertDefaults,
|
||||
version: version || ENTITY_VERSION_V5,
|
||||
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
});
|
||||
}
|
||||
|
||||
queryParams.set(
|
||||
QueryParams.alertType,
|
||||
typ === AlertTypes.ANOMALY_BASED_ALERT
|
||||
? AlertTypes.METRICS_BASED_ALERT
|
||||
: typ,
|
||||
);
|
||||
|
||||
if (
|
||||
typ === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
);
|
||||
} else {
|
||||
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||
}
|
||||
|
||||
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||
history.replace(generatedUrl, {
|
||||
thresholds,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (alertType) {
|
||||
onSelectType(alertType);
|
||||
} else {
|
||||
logEvent('Alert: New alert data source selection page visited', {});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [alertType]);
|
||||
|
||||
if (!initValues) {
|
||||
return (
|
||||
<Row wrap={false}>
|
||||
<SelectAlertType onSelect={onSelectType} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
|
||||
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
|
||||
const version = queryParams.get(QueryParams.version);
|
||||
const showNewCreateAlertsPageFlag =
|
||||
queryParams.get('showNewCreateAlertsPage') === 'true';
|
||||
queryParams.get(QueryParams.showNewCreateAlertsPage) === 'true';
|
||||
|
||||
const alertType = useMemo(() => {
|
||||
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||
}
|
||||
if (!alertTypeFromURL) {
|
||||
const dataSource = compositeQuery?.builder.queryData?.[0]?.dataSource;
|
||||
if (dataSource) {
|
||||
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||
}
|
||||
return AlertTypes.METRICS_BASED_ALERT;
|
||||
}
|
||||
return alertTypeFromURL as AlertTypes;
|
||||
}, [alertTypeFromURL, ruleTypeFromURL, compositeQuery?.builder.queryData]);
|
||||
|
||||
const initialAlertValue: AlertDef = useMemo(
|
||||
() => ({
|
||||
...ALERTS_VALUES_MAP[alertType],
|
||||
version: version || ENTITY_VERSION_V5,
|
||||
}),
|
||||
[alertType, version],
|
||||
);
|
||||
|
||||
if (
|
||||
showNewCreateAlertsPageFlag &&
|
||||
@@ -140,7 +56,7 @@ function CreateRules(): JSX.Element {
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initValues}
|
||||
initialValue={initialAlertValue}
|
||||
ruleId=""
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Select, Tooltip, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { ALL_SELECTED_VALUE } from '../constants';
|
||||
import { useCreateAlertState } from '../context';
|
||||
|
||||
function MultipleNotifications(): JSX.Element {
|
||||
@@ -12,6 +13,12 @@ function MultipleNotifications(): JSX.Element {
|
||||
} = useCreateAlertState();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const isAllOptionSelected = useMemo(
|
||||
() =>
|
||||
notificationSettings.multipleNotifications?.includes(ALL_SELECTED_VALUE),
|
||||
[notificationSettings.multipleNotifications],
|
||||
);
|
||||
|
||||
const spaceAggregationOptions = useMemo(() => {
|
||||
const allGroupBys = currentQuery.builder.queryData?.reduce<string[]>(
|
||||
(acc, query) => {
|
||||
@@ -21,15 +28,60 @@ function MultipleNotifications(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
const uniqueGroupBys = [...new Set(allGroupBys)];
|
||||
return uniqueGroupBys.map((key) => ({
|
||||
const options = uniqueGroupBys.map((key) => ({
|
||||
label: key,
|
||||
value: key,
|
||||
disabled: isAllOptionSelected,
|
||||
'data-testid': 'multiple-notifications-select-option',
|
||||
}));
|
||||
}, [currentQuery.builder.queryData]);
|
||||
if (options.length > 0) {
|
||||
return [
|
||||
{
|
||||
label: 'All',
|
||||
value: ALL_SELECTED_VALUE,
|
||||
'data-testid': 'multiple-notifications-select-option',
|
||||
},
|
||||
...options,
|
||||
];
|
||||
}
|
||||
return options;
|
||||
}, [currentQuery.builder.queryData, isAllOptionSelected]);
|
||||
|
||||
const isMultipleNotificationsEnabled = spaceAggregationOptions.length > 0;
|
||||
|
||||
const onSelectChange = useCallback(
|
||||
(newSelectedOptions: string[]): void => {
|
||||
const currentSelectedOptions = notificationSettings.multipleNotifications;
|
||||
const allOptionLastSelected =
|
||||
!currentSelectedOptions?.includes(ALL_SELECTED_VALUE) &&
|
||||
newSelectedOptions.includes(ALL_SELECTED_VALUE);
|
||||
if (allOptionLastSelected) {
|
||||
setNotificationSettings({
|
||||
type: 'SET_MULTIPLE_NOTIFICATIONS',
|
||||
payload: [ALL_SELECTED_VALUE],
|
||||
});
|
||||
} else {
|
||||
setNotificationSettings({
|
||||
type: 'SET_MULTIPLE_NOTIFICATIONS',
|
||||
payload: newSelectedOptions,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setNotificationSettings, notificationSettings.multipleNotifications],
|
||||
);
|
||||
|
||||
const groupByDescription = useMemo(() => {
|
||||
if (isAllOptionSelected) {
|
||||
return 'All = grouping of alerts is disabled';
|
||||
}
|
||||
if (notificationSettings.multipleNotifications?.length) {
|
||||
return `Alerts with same ${notificationSettings.multipleNotifications?.join(
|
||||
', ',
|
||||
)} will be grouped`;
|
||||
}
|
||||
return 'Empty = all matching alerts combined into one notification';
|
||||
}, [isAllOptionSelected, notificationSettings.multipleNotifications]);
|
||||
|
||||
const multipleNotificationsInput = useMemo(() => {
|
||||
const placeholder = isMultipleNotificationsEnabled
|
||||
? 'Select fields to group by (optional)'
|
||||
@@ -38,12 +90,7 @@ function MultipleNotifications(): JSX.Element {
|
||||
<div>
|
||||
<Select
|
||||
options={spaceAggregationOptions}
|
||||
onChange={(value): void => {
|
||||
setNotificationSettings({
|
||||
type: 'SET_MULTIPLE_NOTIFICATIONS',
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
onChange={onSelectChange}
|
||||
value={notificationSettings.multipleNotifications}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
@@ -54,11 +101,7 @@ function MultipleNotifications(): JSX.Element {
|
||||
/>
|
||||
{isMultipleNotificationsEnabled && (
|
||||
<Typography.Paragraph className="multiple-notifications-select-description">
|
||||
{notificationSettings.multipleNotifications?.length
|
||||
? `Alerts with same ${notificationSettings.multipleNotifications?.join(
|
||||
', ',
|
||||
)} will be grouped`
|
||||
: 'Empty = all matching alerts combined into one notification'}
|
||||
{groupByDescription}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</div>
|
||||
@@ -72,9 +115,10 @@ function MultipleNotifications(): JSX.Element {
|
||||
}
|
||||
return input;
|
||||
}, [
|
||||
groupByDescription,
|
||||
isMultipleNotificationsEnabled,
|
||||
notificationSettings.multipleNotifications,
|
||||
setNotificationSettings,
|
||||
onSelectChange,
|
||||
spaceAggregationOptions,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ALL_SELECTED_VALUE } from 'container/CreateAlertV2/constants';
|
||||
import * as createAlertContext from 'container/CreateAlertV2/context';
|
||||
import {
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
@@ -28,6 +29,7 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
}));
|
||||
|
||||
const TEST_QUERY = 'test-query';
|
||||
const TEST_QUERY_2 = 'test-query-2';
|
||||
const TEST_GROUP_BY_FIELDS = [{ key: 'service' }, { key: 'environment' }];
|
||||
const TRUE = 'true';
|
||||
const FALSE = 'false';
|
||||
@@ -151,7 +153,7 @@ describe('MultipleNotifications', () => {
|
||||
groupBy: [{ key: 'http.status_code' }],
|
||||
},
|
||||
{
|
||||
queryName: 'test-query-2',
|
||||
queryName: TEST_QUERY_2,
|
||||
groupBy: [{ key: 'service' }],
|
||||
},
|
||||
],
|
||||
@@ -167,6 +169,99 @@ describe('MultipleNotifications', () => {
|
||||
expect(
|
||||
screen.getByRole('option', { name: 'http.status_code' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('option', { name: 'service' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selecting the "all" option shows correct group by description', () => {
|
||||
useQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: TEST_QUERY_2,
|
||||
groupBy: [{ key: 'service' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
notificationSettings: {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
multipleNotifications: [ALL_SELECTED_VALUE],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(<MultipleNotifications />);
|
||||
|
||||
expect(
|
||||
screen.getByText('All = grouping of alerts is disabled'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selecting "all" option should disable selection of other options', async () => {
|
||||
useQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: TEST_QUERY_2,
|
||||
groupBy: [{ key: 'service' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<MultipleNotifications />);
|
||||
|
||||
const select = screen.getByRole(COMBOBOX_ROLE);
|
||||
await userEvent.click(select);
|
||||
|
||||
const serviceOption = screen.getAllByTestId(
|
||||
'multiple-notifications-select-option',
|
||||
);
|
||||
expect(serviceOption).toHaveLength(2);
|
||||
expect(serviceOption[0]).not.toHaveClass('ant-select-item-option-disabled');
|
||||
expect(serviceOption[1]).toHaveClass('ant-select-item-option-disabled');
|
||||
});
|
||||
|
||||
it('selecting "all" option should remove all other selected options', async () => {
|
||||
useQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: TEST_QUERY_2,
|
||||
groupBy: [{ key: 'service' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
notificationSettings: {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
multipleNotifications: ['service'],
|
||||
},
|
||||
setNotificationSettings: mockSetNotificationSettings,
|
||||
}),
|
||||
);
|
||||
|
||||
render(<MultipleNotifications />);
|
||||
const select = screen.getByRole(COMBOBOX_ROLE);
|
||||
await userEvent.click(select);
|
||||
|
||||
const serviceOption = screen.getAllByTestId(
|
||||
'multiple-notifications-select-option',
|
||||
);
|
||||
expect(serviceOption).toHaveLength(2);
|
||||
await userEvent.click(serviceOption[0]);
|
||||
expect(mockSetNotificationSettings).toHaveBeenCalledWith({
|
||||
type: 'SET_MULTIPLE_NOTIFICATIONS',
|
||||
payload: [ALL_SELECTED_VALUE],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,24 +35,21 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||
|
||||
const yAxisUnit = alertState.yAxisUnit || '';
|
||||
|
||||
const fetchYAxisUnit =
|
||||
const shouldUpdateYAxisUnit =
|
||||
!isEditMode && alertType === AlertTypes.METRICS_BASED_ALERT;
|
||||
|
||||
const selectedQueryName = thresholdState.selectedQuery;
|
||||
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit(
|
||||
selectedQueryName,
|
||||
{
|
||||
enabled: fetchYAxisUnit,
|
||||
},
|
||||
);
|
||||
|
||||
// Every time a new metric is selected, set the y-axis unit to its unit value if present
|
||||
// Only for metrics-based alerts
|
||||
// Only for metrics-based alerts in create mode
|
||||
useEffect(() => {
|
||||
if (fetchYAxisUnit) {
|
||||
if (shouldUpdateYAxisUnit) {
|
||||
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: initialYAxisUnit });
|
||||
}
|
||||
}, [initialYAxisUnit, setAlertState, fetchYAxisUnit]);
|
||||
}, [initialYAxisUnit, setAlertState, shouldUpdateYAxisUnit]);
|
||||
|
||||
const headline = (
|
||||
<div className="chart-preview-headline">
|
||||
|
||||
@@ -72,3 +72,5 @@ export const defaultPostableAlertRuleV2: PostableAlertRuleV2 = {
|
||||
alert: 'TEST_ALERT',
|
||||
evaluation: defaultEvaluation,
|
||||
};
|
||||
|
||||
export const ALL_SELECTED_VALUE = '__all__';
|
||||
|
||||
@@ -1,47 +1,3 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid #161922;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-container {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
@@ -576,28 +532,6 @@
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-container {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import './SettingsDrawer.styles.scss';
|
||||
|
||||
import { Drawer } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { memo, PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
type SettingsDrawerProps = PropsWithChildren<{
|
||||
drawerTitle: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsDrawer({
|
||||
children,
|
||||
drawerTitle,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: SettingsDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
|
||||
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SettingsDrawer);
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -52,9 +53,11 @@ import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import DashboardGraphSlider from '../ComponentsSlider';
|
||||
import DashboardSettings from '../DashboardSettings';
|
||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import { VariablesSettingsTab } from './types';
|
||||
import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils';
|
||||
|
||||
interface DashboardDescriptionProps {
|
||||
@@ -101,6 +104,11 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
@@ -340,6 +348,18 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
publicDashboardResponse?.data,
|
||||
]);
|
||||
|
||||
const onConfigureClick = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const onSettingsDrawerClose = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(false);
|
||||
// good use case for a state library like Jotai
|
||||
if (variablesSettingsTabHandle.current) {
|
||||
variablesSettingsTabHandle.current.resetState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="dashboard-description-container">
|
||||
<div className="dashboard-header">
|
||||
@@ -504,7 +524,26 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
/>
|
||||
</Popover>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={onConfigureClick}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={onSettingsDrawerClose}
|
||||
>
|
||||
<DashboardSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
@@ -0,0 +1,7 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
export interface VariablesSettingsTab {
|
||||
resetState: () => void;
|
||||
}
|
||||
|
||||
export type VariablesSettingsTabHandle = MutableRefObject<VariablesSettingsTab | null>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user