mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-05 05:11:59 +00:00
Compare commits
19 Commits
v0.107.0
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
543fe613a5 | ||
|
|
512e0519ee | ||
|
|
d56ab18691 | ||
|
|
9d95703539 | ||
|
|
7dc54530ce | ||
|
|
e712434e01 | ||
|
|
91ec60b923 | ||
|
|
325974292f | ||
|
|
7c1a531d01 | ||
|
|
5a45532a72 | ||
|
|
e9501d2e0f | ||
|
|
c306e66bcd | ||
|
|
767a0cc28e | ||
|
|
4f9efcc133 | ||
|
|
bf2dd612e0 | ||
|
|
d3f15022a4 | ||
|
|
a5c021e96c | ||
|
|
8b9fcae0cb | ||
|
|
e75abcb108 |
52
.github/CODEOWNERS
vendored
52
.github/CODEOWNERS
vendored
@@ -16,13 +16,13 @@
|
||||
|
||||
# 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
|
||||
|
||||
@@ -48,19 +48,53 @@
|
||||
/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
|
||||
|
||||
|
||||
# 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 @therealpandey
|
||||
/pkg/authz/ @vikrantgupta25
|
||||
|
||||
# Integration tests
|
||||
|
||||
/tests/integration/ @therealpandey
|
||||
/tests/integration/ @vikrantgupta25
|
||||
|
||||
# Dashboard Owners
|
||||
|
||||
|
||||
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 -->
|
||||
|
||||
---
|
||||
|
||||
2
.gitignore
vendored
2
.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__/
|
||||
|
||||
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
|
||||
}
|
||||
4
context7.json
Normal file
4
context7.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"url": "https://context7.com/signoz/signoz",
|
||||
"public_key": "pk_6g9GfjdkuPEIDuTGAxnol"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
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 |
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;
|
||||
@@ -58,6 +58,7 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-left: 30px;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -53,4 +53,5 @@ export enum QueryParams {
|
||||
variables = 'variables',
|
||||
version = 'version',
|
||||
showNewCreateAlertsPage = 'showNewCreateAlertsPage',
|
||||
source = 'source',
|
||||
}
|
||||
|
||||
@@ -301,6 +301,7 @@ export const initialQueryState: QueryState = {
|
||||
builder: initialQueryBuilderData,
|
||||
clickhouse_sql: [initialClickHouseData],
|
||||
promql: [initialQueryPromQLData],
|
||||
unit: '',
|
||||
};
|
||||
|
||||
const initialQueryWithType: Query = {
|
||||
|
||||
@@ -788,11 +788,18 @@ function FormAlertRules({
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||
?.active || false;
|
||||
|
||||
const source = useMemo(() => urlQuery.get(QueryParams.source) as YAxisSource, [
|
||||
urlQuery,
|
||||
]);
|
||||
|
||||
// Only update automatically when creating a new metrics-based alert rule
|
||||
const shouldUpdateYAxisUnit = useMemo(
|
||||
() => isNewRule && alertType === AlertTypes.METRICS_BASED_ALERT,
|
||||
[isNewRule, alertType],
|
||||
);
|
||||
const shouldUpdateYAxisUnit = useMemo(() => {
|
||||
// Do not update if we are coming to the page from dashboards (we still show warning)
|
||||
if (source === YAxisSource.DASHBOARDS) {
|
||||
return false;
|
||||
}
|
||||
return isNewRule && alertType === AlertTypes.METRICS_BASED_ALERT;
|
||||
}, [isNewRule, alertType, source]);
|
||||
|
||||
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit(
|
||||
alertDef.condition.selectedQueryName,
|
||||
|
||||
@@ -1,44 +1,419 @@
|
||||
@import '@signozhq/design-tokens/dist/style.css';
|
||||
|
||||
.login-form-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.login-form-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.login-form-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-form-header-text {
|
||||
color: var(--text-vanilla-300);
|
||||
}
|
||||
.login-form-emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
padding: 0px 16px;
|
||||
}
|
||||
.login-form-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
height: 40px;
|
||||
}
|
||||
.login-form-description {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
max-width: 317px;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-acccount {
|
||||
color: var(--text-vanilla-300);
|
||||
font-size: 12px;
|
||||
margin-top: 16px;
|
||||
.login-form-card {
|
||||
width: 100%;
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.login-error-container {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.login-error-icon {
|
||||
color: var(--bg-cherry-200);
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.login-form-container {
|
||||
.login-form-header {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
.login-error-container {
|
||||
.error-content {
|
||||
background: rgba(229, 72, 77, 0.1);
|
||||
border-color: rgba(229, 72, 77, 0.2);
|
||||
|
||||
.login-form-header-text {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
&__error-code {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
.no-acccount {
|
||||
color: var(--text-ink-500);
|
||||
&__error-message {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&__docs-button {
|
||||
color: var(--bg-ink-400);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-100);
|
||||
border-color: var(--bg-vanilla-400, #d1d5db);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__message-badge-label-text {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
&__message-item {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&::before {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-hint-text {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-label-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
|
||||
> label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--text-neutral-dark-200) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-neutral-dark-300) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
height: 32px;
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
border-radius: 2px;
|
||||
padding: 6px 8px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// Select component styling to match Input
|
||||
&.ant-select {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.ant-select-selector {
|
||||
height: 32px !important;
|
||||
min-height: 32px !important;
|
||||
padding: 0 8px !important;
|
||||
border-radius: 2px !important;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 1 !important;
|
||||
letter-spacing: -0.065px !important;
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-search {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ant-select-selection-item,
|
||||
.ant-select-selection-placeholder {
|
||||
line-height: 20px !important;
|
||||
height: 20px !important;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
letter-spacing: -0.065px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--levels-l3-border, #2c303a) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove border for orgId Select only
|
||||
&.login-form-select-no-border {
|
||||
&.ant-select .ant-select-selector {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&.ant-select:hover .ant-select-selector {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&.ant-select.ant-select-focused .ant-select-selector {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-form-actions {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.login-submit-btn {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--semantic-primary-foreground, #eceef2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.login-form-title {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
.login-form-description {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.login-form-card {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
color: var(--text-neutral-dark-300);
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-ink-500);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
|
||||
// Select component light mode styling
|
||||
&.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('Login Component', () => {
|
||||
).toBeInTheDocument();
|
||||
expect(getByTestId('email')).toBeInTheDocument();
|
||||
expect(getByTestId('initiate_login')).toBeInTheDocument();
|
||||
expect(getByPlaceholderText('name@yourcompany.com')).toBeInTheDocument();
|
||||
expect(getByPlaceholderText('e.g. john@signoz.io')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state when version data is being fetched', () => {
|
||||
@@ -213,19 +213,27 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgPasswordAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -253,10 +261,21 @@ describe('Login Component', () => {
|
||||
|
||||
const { getByTestId, getByText } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -269,19 +288,27 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgPasswordAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -298,25 +325,33 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockMultiOrgMixedAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockMultiOrgMixedAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId, getByText } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Organization Name')).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByRole('combobox');
|
||||
|
||||
// Click on the dropdown to reveal the options
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
@@ -338,25 +373,30 @@ describe('Login Component', () => {
|
||||
|
||||
render(<Login />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
const nextButton = screen.getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = screen.getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = screen.getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByRole('combobox');
|
||||
|
||||
// Select CALLBACK_AUTHN_ORG
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
await user.click(screen.getByText(CALLBACK_AUTHN_ORG));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /login with callback/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByRole('button', { name: /sign in with sso/i });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -366,19 +406,27 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgPasswordAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId, getByText } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -393,10 +441,7 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgCallbackAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgCallbackAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -404,10 +449,21 @@ describe('Login Component', () => {
|
||||
initialRoute: '/login?password=Y',
|
||||
});
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -423,19 +479,27 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgCallbackAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgCallbackAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId, queryByTestId } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -458,19 +522,27 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgCallbackAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgCallbackAuth })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId, queryByTestId } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -494,10 +566,7 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgPasswordAuth })),
|
||||
),
|
||||
rest.post('*/api/v2/sessions/email_password', async (_, res, ctx) =>
|
||||
res(
|
||||
@@ -509,10 +578,21 @@ describe('Login Component', () => {
|
||||
|
||||
const { getByTestId } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -537,10 +617,7 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockSingleOrgPasswordAuth })),
|
||||
),
|
||||
rest.post('*/api/v2/sessions/email_password', (_, res, ctx) =>
|
||||
res(
|
||||
@@ -558,10 +635,21 @@ describe('Login Component', () => {
|
||||
|
||||
const { getByTestId, getByText } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -599,7 +687,7 @@ describe('Login Component', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('AUTH_ERROR')).toBeInTheDocument();
|
||||
expect(getByText('Authentication failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -611,7 +699,7 @@ describe('Login Component', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('invalid-json')).not.toBeInTheDocument();
|
||||
expect(getByText('AUTH_ERROR')).toBeInTheDocument();
|
||||
expect(getByText('Authentication failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -622,19 +710,27 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockOrgWithWarning }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockOrgWithWarning })),
|
||||
),
|
||||
);
|
||||
|
||||
render(<Login />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
const nextButton = screen.getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = screen.getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = screen.getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -666,24 +762,30 @@ describe('Login Component', () => {
|
||||
|
||||
server.use(
|
||||
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: mockMultiOrgWithWarning }),
|
||||
),
|
||||
res(ctx.status(200), ctx.json({ data: mockMultiOrgWithWarning })),
|
||||
),
|
||||
);
|
||||
|
||||
const { getByTestId } = render(<Login />);
|
||||
|
||||
const emailInput = getByTestId('email');
|
||||
const nextButton = getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByRole('combobox');
|
||||
|
||||
// Select the organization with a warning
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
@@ -713,10 +815,21 @@ describe('Login Component', () => {
|
||||
|
||||
render(<Login />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
const nextButton = screen.getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = screen.getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = screen.getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
// Button should be disabled during API call
|
||||
@@ -737,14 +850,25 @@ describe('Login Component', () => {
|
||||
// Initially shows "Next" button
|
||||
expect(screen.getByTestId('initiate_login')).toBeInTheDocument();
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
const nextButton = screen.getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = screen.getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = screen.getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show "Login" button for password auth
|
||||
// Should show "Sign in with Password" button for password auth
|
||||
expect(screen.getByTestId('password_authn_submit')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('initiate_login')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -768,10 +892,21 @@ describe('Login Component', () => {
|
||||
|
||||
render(<Login />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
const nextButton = screen.getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = screen.getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = screen.getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -810,10 +945,21 @@ describe('Login Component', () => {
|
||||
|
||||
render(<Login />);
|
||||
|
||||
const emailInput = screen.getByTestId('email');
|
||||
const nextButton = screen.getByTestId('initiate_login');
|
||||
// Wait for version API to complete (email input becomes enabled)
|
||||
const emailInput = await waitFor(() => {
|
||||
const input = screen.getByTestId('email');
|
||||
expect(input).not.toBeDisabled();
|
||||
return input;
|
||||
});
|
||||
|
||||
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||
|
||||
const nextButton = await waitFor(() => {
|
||||
const button = screen.getByTestId('initiate_login');
|
||||
expect(button).not.toBeDisabled();
|
||||
return button;
|
||||
});
|
||||
|
||||
await user.click(nextButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import './Login.styles.scss';
|
||||
|
||||
import { Button, Form, Input, Select, Space, Tooltip, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Form, Input, Select, Tooltip, Typography } from 'antd';
|
||||
import getVersion from 'api/v1/version/get';
|
||||
import get from 'api/v2/sessions/context/get';
|
||||
import post from 'api/v2/sessions/email_password/post';
|
||||
import afterLogin from 'AppRoutes/utils';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ErrorV2 } from 'types/api';
|
||||
@@ -37,6 +38,7 @@ type FormValues = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function Login(): JSX.Element {
|
||||
const urlQueryParams = useUrlQuery();
|
||||
// override for callbackAuthN in case of some misconfiguration
|
||||
@@ -61,7 +63,12 @@ function Login(): JSX.Element {
|
||||
setIsLoadingSessionsContext,
|
||||
] = useState<boolean>(false);
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const [errorMessage, setErrorMessage] = useState<APIError>();
|
||||
|
||||
// Watch form values for validation
|
||||
const email = Form.useWatch('email', form);
|
||||
const password = Form.useWatch('password', form);
|
||||
const orgId = Form.useWatch('orgId', form);
|
||||
|
||||
// setupCompleted information to route to signup page in case setup is incomplete
|
||||
const {
|
||||
@@ -90,6 +97,7 @@ function Login(): JSX.Element {
|
||||
const onNextHandler = async (): Promise<void> => {
|
||||
const email = form.getFieldValue('email');
|
||||
setIsLoadingSessionsContext(true);
|
||||
setErrorMessage(undefined);
|
||||
|
||||
try {
|
||||
const sessionsContextResponse = await get({
|
||||
@@ -102,7 +110,7 @@ function Login(): JSX.Element {
|
||||
setSessionsOrgId(sessionsContextResponse.data.orgs[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setErrorMessage(error as APIError);
|
||||
}
|
||||
setIsLoadingSessionsContext(false);
|
||||
};
|
||||
@@ -181,6 +189,7 @@ function Login(): JSX.Element {
|
||||
|
||||
const onSubmitHandler: () => Promise<void> = async () => {
|
||||
setIsSubmitting(true);
|
||||
setErrorMessage(undefined);
|
||||
|
||||
try {
|
||||
if (isPasswordAuthN) {
|
||||
@@ -205,7 +214,7 @@ function Login(): JSX.Element {
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setErrorMessage(error as APIError);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@@ -213,7 +222,7 @@ function Login(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (callbackAuthError) {
|
||||
showErrorModal(
|
||||
setErrorMessage(
|
||||
new APIError({
|
||||
httpStatusCode: 500,
|
||||
error: {
|
||||
@@ -231,110 +240,140 @@ function Login(): JSX.Element {
|
||||
callbackAuthErrorCode,
|
||||
callbackAuthErrorMessage,
|
||||
callbackAuthErrorURL,
|
||||
showErrorModal,
|
||||
setErrorMessage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionsOrgWarning) {
|
||||
showErrorModal(
|
||||
setErrorMessage(
|
||||
new APIError({
|
||||
httpStatusCode: 400,
|
||||
error: {
|
||||
code: sessionsOrgWarning.code,
|
||||
message: sessionsOrgWarning.message,
|
||||
url: sessionsOrgWarning.url,
|
||||
errors: sessionsOrgWarning.errors,
|
||||
},
|
||||
httpStatusCode: 400,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [sessionsOrgWarning, showErrorModal]);
|
||||
}, [sessionsOrgWarning, setErrorMessage]);
|
||||
|
||||
// Validation helpers
|
||||
const isEmailValid = Boolean(
|
||||
email?.trim() && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
|
||||
);
|
||||
|
||||
const isNextButtonEnabled =
|
||||
isEmailValid && !versionLoading && !sessionsContextLoading;
|
||||
|
||||
const isSubmitButtonEnabled = useMemo((): boolean => {
|
||||
if (!isEmailValid || isSubmitting) return false;
|
||||
const hasMultipleOrgs = (sessionsContext?.orgs.length ?? 0) > 1;
|
||||
if (hasMultipleOrgs && !orgId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(isPasswordAuthN && !password?.trim());
|
||||
}, [
|
||||
isEmailValid,
|
||||
isSubmitting,
|
||||
sessionsContext,
|
||||
orgId,
|
||||
isPasswordAuthN,
|
||||
password,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="login-form-container">
|
||||
<FormContainer form={form} onFinish={onSubmitHandler}>
|
||||
<div className="login-form-header">
|
||||
<Typography.Paragraph className="login-form-header-text">
|
||||
<div className="login-form-emoji">
|
||||
<img src="/svgs/tv.svg" alt="TV" width="32" height="32" />
|
||||
</div>
|
||||
<Typography.Title level={4} className="login-form-title">
|
||||
Sign in to your workspace
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="login-form-description">
|
||||
Sign in to monitor, trace, and troubleshoot your applications
|
||||
effortlessly.
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<ParentContainer>
|
||||
<Label htmlFor="signupEmail" style={{ marginTop: 0 }}>
|
||||
Email
|
||||
</Label>
|
||||
<FormContainer.Item name="email">
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
data-testid="email"
|
||||
required
|
||||
placeholder="name@yourcompany.com"
|
||||
autoFocus
|
||||
disabled={versionLoading}
|
||||
className="login-form-input"
|
||||
onPressEnter={onNextHandler}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</ParentContainer>
|
||||
|
||||
{sessionsContext && sessionsContext.orgs.length > 1 && (
|
||||
<div className="login-form-card">
|
||||
<ParentContainer>
|
||||
<Label htmlFor="orgId">Organization Name</Label>
|
||||
<FormContainer.Item name="orgId">
|
||||
<Select
|
||||
id="orgId"
|
||||
data-testid="orgId"
|
||||
className="login-form-input"
|
||||
placeholder="Select your organization"
|
||||
options={sessionsContext.orgs.map((org) => ({
|
||||
value: org.id,
|
||||
label: org.name || 'default',
|
||||
}))}
|
||||
onChange={(value: string): void => {
|
||||
setSessionsOrgId(value);
|
||||
}}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</ParentContainer>
|
||||
)}
|
||||
|
||||
{sessionsContext && isPasswordAuthN && (
|
||||
<ParentContainer>
|
||||
<Label htmlFor="Password">Password</Label>
|
||||
<FormContainer.Item name="password">
|
||||
<Input.Password
|
||||
<Label htmlFor="signupEmail">Email address</Label>
|
||||
<FormContainer.Item name="email">
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
data-testid="email"
|
||||
required
|
||||
id="currentPassword"
|
||||
data-testid="password"
|
||||
disabled={isSubmitting}
|
||||
placeholder="e.g. john@signoz.io"
|
||||
autoFocus
|
||||
disabled={versionLoading}
|
||||
className="login-form-input"
|
||||
onPressEnter={onNextHandler}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Tooltip title="Ask your admin to reset your password and send you a new invite link">
|
||||
<Typography.Link>Forgot password?</Typography.Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ParentContainer>
|
||||
)}
|
||||
|
||||
<Space
|
||||
style={{ marginTop: 16 }}
|
||||
align="start"
|
||||
direction="vertical"
|
||||
size={20}
|
||||
>
|
||||
{sessionsContext && sessionsContext.orgs.length > 1 && (
|
||||
<ParentContainer>
|
||||
<Label htmlFor="orgId">Organization Name</Label>
|
||||
<FormContainer.Item name="orgId">
|
||||
<Select
|
||||
id="orgId"
|
||||
data-testid="orgId"
|
||||
className="login-form-input login-form-select-no-border"
|
||||
placeholder="Select your organization"
|
||||
options={sessionsContext.orgs.map((org) => ({
|
||||
value: org.id,
|
||||
label: org.name || 'default',
|
||||
}))}
|
||||
onChange={(value: string): void => {
|
||||
setSessionsOrgId(value);
|
||||
}}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</ParentContainer>
|
||||
)}
|
||||
|
||||
{sessionsContext && isPasswordAuthN && (
|
||||
<ParentContainer>
|
||||
<div className="password-label-container">
|
||||
<Label htmlFor="Password">Password</Label>
|
||||
<Tooltip title="Ask your admin to reset your password and send you a new invite link">
|
||||
<Typography.Link className="forgot-password-link">
|
||||
Forgot password?
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<FormContainer.Item name="password">
|
||||
<Input.Password
|
||||
required
|
||||
placeholder="Enter password"
|
||||
id="currentPassword"
|
||||
data-testid="password"
|
||||
disabled={isSubmitting}
|
||||
className="login-form-input"
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</ParentContainer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{errorMessage && <AuthError error={errorMessage} />}
|
||||
|
||||
<div className="login-form-actions">
|
||||
{!sessionsContext && (
|
||||
<Button
|
||||
disabled={versionLoading || sessionsContextLoading}
|
||||
type="primary"
|
||||
disabled={!isNextButtonEnabled}
|
||||
variant="solid"
|
||||
onClick={onNextHandler}
|
||||
data-testid="initiate_login"
|
||||
className="periscope-btn primary next-btn"
|
||||
icon={<ArrowRight size={12} />}
|
||||
className="login-submit-btn"
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
@@ -342,32 +381,34 @@ function Login(): JSX.Element {
|
||||
|
||||
{sessionsContext && isCallbackAuthN && (
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={!isSubmitButtonEnabled}
|
||||
variant="solid"
|
||||
type="submit"
|
||||
color="primary"
|
||||
data-testid="callback_authn_submit"
|
||||
data-attr="signup"
|
||||
className="periscope-btn primary next-btn"
|
||||
icon={<ArrowRight size={12} />}
|
||||
className="login-submit-btn"
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Login With Callback
|
||||
Sign in with SSO
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{sessionsContext && isPasswordAuthN && (
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
type="primary"
|
||||
disabled={!isSubmitButtonEnabled}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
data-testid="password_authn_submit"
|
||||
htmlType="submit"
|
||||
type="submit"
|
||||
data-attr="signup"
|
||||
className="periscope-btn primary next-btn"
|
||||
icon={<ArrowRight size={12} />}
|
||||
className="login-submit-btn"
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Login
|
||||
Sign in with Password
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { Card, Form } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FormWrapper = styled(Card)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 390px;
|
||||
min-height: 430px;
|
||||
max-width: 432px;
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
&&&.ant-card-body {
|
||||
min-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Label = styled.label`
|
||||
margin-bottom: 11px;
|
||||
margin-top: 19px;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
line-height: 24px;
|
||||
font-family: var(--font-family-inter, Inter), sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FormContainer = styled(Form)`
|
||||
@@ -30,9 +24,58 @@ export const FormContainer = styled(Form)`
|
||||
|
||||
& .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
width: 100%;
|
||||
|
||||
& .ant-select {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& .ant-form-item-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& .ant-form-item-control-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& .ant-form-item-control-input-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input,
|
||||
& .ant-input-password,
|
||||
& .ant-select-selector {
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border-color: var(--levels-l3-border, #2c303a) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
|
||||
.lightMode & {
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input:focus,
|
||||
& .ant-input-password:focus,
|
||||
& .ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ParentContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
@@ -80,14 +80,14 @@ describe('splitQueryIntoOneChartPerQuery', () => {
|
||||
expect(result[2].builder.queryData).toHaveLength(2); // 2 disabled queries
|
||||
expect(result[2].builder.queryData[0].disabled).toBe(true);
|
||||
expect(result[2].builder.queryData[1].disabled).toBe(true);
|
||||
expect(result[2].unit).toBeUndefined();
|
||||
expect(result[2].unit).toBe('');
|
||||
// Verify query 4 has the correct data
|
||||
expect(result[3].builder.queryFormulas).toHaveLength(1);
|
||||
expect(result[3].builder.queryFormulas[0]).toEqual(MOCK_FORMULA_DATA);
|
||||
expect(result[3].builder.queryData).toHaveLength(2); // 2 disabled queries
|
||||
expect(result[3].builder.queryData[0].disabled).toBe(true);
|
||||
expect(result[3].builder.queryData[1].disabled).toBe(true);
|
||||
expect(result[3].unit).toBeUndefined();
|
||||
expect(result[3].unit).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ const compositeQueryParam = {
|
||||
},
|
||||
],
|
||||
id: '12e1d311-cb47-4b76-af68-65d8e85c9e0d',
|
||||
unit: '',
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import '../OnboardingQuestionaire.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Checkbox, Input, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Input } from '@signozhq/input';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
|
||||
|
||||
export interface SignozDetails {
|
||||
interestInSignoz: string[] | null;
|
||||
otherInterestInSignoz: string | null;
|
||||
@@ -18,7 +21,6 @@ interface AboutSigNozQuestionsProps {
|
||||
signozDetails: SignozDetails;
|
||||
setSignozDetails: (details: SignozDetails) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const interestedInOptions: Record<string, string> = {
|
||||
@@ -34,7 +36,6 @@ export function AboutSigNozQuestions({
|
||||
signozDetails,
|
||||
setSignozDetails,
|
||||
onNext,
|
||||
onBack,
|
||||
}: AboutSigNozQuestionsProps): JSX.Element {
|
||||
const [interestInSignoz, setInterestInSignoz] = useState<string[]>(
|
||||
signozDetails?.interestInSignoz || [],
|
||||
@@ -67,6 +68,12 @@ export function AboutSigNozQuestions({
|
||||
}
|
||||
};
|
||||
|
||||
const createInterestChangeHandler = (option: string) => (
|
||||
checked: boolean,
|
||||
): void => {
|
||||
handleInterestChange(option, Boolean(checked));
|
||||
};
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
setSignozDetails({
|
||||
discoverSignoz,
|
||||
@@ -83,24 +90,12 @@ export function AboutSigNozQuestions({
|
||||
onNext();
|
||||
};
|
||||
|
||||
const handleOnBack = (): void => {
|
||||
setSignozDetails({
|
||||
discoverSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
onBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
Tell Us About Your Interest in SigNoz
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
We'd love to know a little bit about you and your interest in SigNoz
|
||||
</Typography.Paragraph>
|
||||
<OnboardingQuestionHeader
|
||||
title="Set up your workspace"
|
||||
subtitle="Tailor SigNoz to suit your observability needs."
|
||||
/>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
@@ -123,37 +118,28 @@ export function AboutSigNozQuestions({
|
||||
{Object.keys(interestedInOptions).map((option: string) => (
|
||||
<div key={option} className="checkbox-item">
|
||||
<Checkbox
|
||||
id={`checkbox-${option}`}
|
||||
checked={interestInSignoz.includes(option)}
|
||||
onChange={(e): void => handleInterestChange(option, e.target.checked)}
|
||||
>
|
||||
{interestedInOptions[option]}
|
||||
</Checkbox>
|
||||
onCheckedChange={createInterestChangeHandler(option)}
|
||||
labelName={interestedInOptions[option]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="checkbox-item">
|
||||
<div className="checkbox-item checkbox-item-others">
|
||||
<Checkbox
|
||||
id="others-checkbox"
|
||||
checked={interestInSignoz.includes('Others')}
|
||||
onChange={(e): void =>
|
||||
handleInterestChange('Others', e.target.checked)
|
||||
}
|
||||
>
|
||||
Others
|
||||
</Checkbox>
|
||||
onCheckedChange={createInterestChangeHandler('Others')}
|
||||
labelName={interestInSignoz.includes('Others') ? '' : 'Others'}
|
||||
/>
|
||||
{interestInSignoz.includes('Others') && (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="Please specify your interest"
|
||||
placeholder="What got you interested in SigNoz?"
|
||||
value={otherInterestInSignoz}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherInterestInSignoz !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onChange={(e): void => setOtherInterestInSignoz(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
@@ -162,20 +148,16 @@ export function AboutSigNozQuestions({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button type="default" className="next-button" onClick={handleOnBack}>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<div className="onboarding-buttons-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className={`next-button ${isNextDisabled ? 'disabled' : ''}`}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={`onboarding-next-button ${isNextDisabled ? 'disabled' : ''}`}
|
||||
onClick={handleOnNext}
|
||||
disabled={isNextDisabled}
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Next
|
||||
<ArrowRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,302 @@
|
||||
.team-member-container {
|
||||
.invite-team-members-table {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.invite-team-members-table-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
|
||||
.team-member-role-select {
|
||||
width: 20%;
|
||||
> div:first-child {
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border: 1px solid #1d212d;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
> div:nth-child(2) {
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
flex: 0 0 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.table-header-cell {
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-team-members-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px !important;
|
||||
width: 100%;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.invite-team-members-error-callout {
|
||||
background: rgba(229, 72, 77, 0.1);
|
||||
border: 1px solid rgba(229, 72, 77, 0.2);
|
||||
border-radius: 4px;
|
||||
animation: horizontal-shaking 300ms ease-out;
|
||||
}
|
||||
|
||||
.team-member-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
> div:first-child {
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
flex: 0 0 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&.email-cell {
|
||||
width: 180px;
|
||||
flex: 0 0 180px;
|
||||
}
|
||||
|
||||
&.role-cell {
|
||||
flex: 1 0 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.action-cell {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
flex: 0 0 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
width: 100%;
|
||||
height: 32px !important;
|
||||
|
||||
height: 32px !important;
|
||||
border-radius: 2px;
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
padding: 6px 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-role-select {
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selector {
|
||||
height: 32px !important;
|
||||
border-radius: 2px !important;
|
||||
background: var(--levels-l3-background, #23262e) !important;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a) !important;
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 1 !important;
|
||||
letter-spacing: -0.065px !important;
|
||||
padding: 0 8px !important;
|
||||
box-sizing: border-box !important;
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
width: 80%;
|
||||
background-color: #121317;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
.ant-select-arrow {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-input-group-addon {
|
||||
background-color: #121317 !important;
|
||||
border-right: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-team-member-button {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
background: transparent !important;
|
||||
color: #e5484d !important;
|
||||
opacity: 0.6 !important;
|
||||
cursor: pointer;
|
||||
padding: 0 !important;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
box-shadow: none !important;
|
||||
|
||||
svg {
|
||||
color: #e5484d !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.1) !important;
|
||||
opacity: 0.9 !important;
|
||||
color: #e5484d !important;
|
||||
|
||||
svg {
|
||||
color: #e5484d !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7 !important;
|
||||
background: rgba(229, 72, 77, 0.15) !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--semantic-primary-background, #4e74f8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.email-error-message,
|
||||
.role-error-message {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
|
||||
.invite-team-members-add-another-member-container {
|
||||
width: 100%;
|
||||
display: flex !important;
|
||||
justify-content: flex-start !important;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.add-another-member-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px dashed var(--semantic-secondary-border, #23262e) !important;
|
||||
background: transparent !important;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
height: auto !important;
|
||||
padding: 6px 8px;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--semantic-secondary-foreground, #adb4c2) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
button,
|
||||
& {
|
||||
border: 1px dashed var(--semantic-secondary-border, #23262e) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-style: dashed !important;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
|
||||
button,
|
||||
& {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-style: dashed !important;
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--semantic-primary-background, #4e74f8);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.questions-form-container {
|
||||
@@ -85,19 +356,112 @@
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.team-member-container {
|
||||
.team-member-role-select {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
.invite-team-members-table-header {
|
||||
.table-header-cell {
|
||||
color: var(--semantic-secondary-foreground, #747b8b);
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500, #1a1d26) !important;
|
||||
|
||||
input {
|
||||
background: var(--bg-vanilla-200, #f5f5f5) !important;
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
color: var(--text-ink-500, #1a1d26) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d) !important;
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-input-group-addon {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-role-select {
|
||||
.ant-select-selector {
|
||||
background: var(--levels-l3-background, #ffffff) !important;
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9) !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--levels-l3-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-team-member-button {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 101, 101, 0.1) !important;
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500, #f56565) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-another-member-button {
|
||||
border: 1px dashed var(--semantic-secondary-border, #e9e9e9) !important;
|
||||
background: transparent !important;
|
||||
color: var(--semantic-secondary-foreground, #747b8b);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--semantic-secondary-foreground, #747b8b) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--semantic-primary-background, #4e74f8) !important;
|
||||
border-style: dashed !important;
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
background: rgba(78, 116, 248, 0.1) !important;
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,3 +484,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes horizontal-shaking {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import './InviteTeamMembers.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Select, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Select, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import inviteUsers from 'api/v1/invite/bulk/create';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
ChevronDown,
|
||||
CircleAlert,
|
||||
Loader2,
|
||||
Plus,
|
||||
TriangleAlert,
|
||||
X,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import APIError from 'types/api/error';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
|
||||
|
||||
interface TeamMember {
|
||||
email: string;
|
||||
role: string;
|
||||
@@ -33,7 +37,6 @@ interface InviteTeamMembersProps {
|
||||
teamMembers: TeamMember[] | null;
|
||||
setTeamMembers: (teamMembers: TeamMember[]) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
function InviteTeamMembers({
|
||||
@@ -41,7 +44,6 @@ function InviteTeamMembers({
|
||||
teamMembers,
|
||||
setTeamMembers,
|
||||
onNext,
|
||||
onBack,
|
||||
}: InviteTeamMembersProps): JSX.Element {
|
||||
const [teamMembersToInvite, setTeamMembersToInvite] = useState<
|
||||
TeamMember[] | null
|
||||
@@ -50,11 +52,13 @@ function InviteTeamMembers({
|
||||
{},
|
||||
);
|
||||
const [hasInvalidEmails, setHasInvalidEmails] = useState<boolean>(false);
|
||||
const [hasInvalidRoles, setHasInvalidRoles] = useState<boolean>(false);
|
||||
const [inviteError, setInviteError] = useState<APIError | null>(null);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const defaultTeamMember: TeamMember = {
|
||||
email: '',
|
||||
role: 'EDITOR',
|
||||
role: '',
|
||||
name: '',
|
||||
frontendBaseUrl: window.location.origin,
|
||||
id: '',
|
||||
@@ -62,12 +66,12 @@ function InviteTeamMembers({
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(teamMembers)) {
|
||||
const teamMember = {
|
||||
const initialTeamMembers = Array.from({ length: 3 }, () => ({
|
||||
...defaultTeamMember,
|
||||
id: uuid(),
|
||||
};
|
||||
}));
|
||||
|
||||
setTeamMembersToInvite([teamMember]);
|
||||
setTeamMembersToInvite(initialTeamMembers);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [teamMembers]);
|
||||
@@ -87,19 +91,32 @@ function InviteTeamMembers({
|
||||
// Validation function to check all users
|
||||
const validateAllUsers = (): boolean => {
|
||||
let isValid = true;
|
||||
let hasEmailErrors = false;
|
||||
let hasRoleErrors = false;
|
||||
|
||||
const updatedValidity: Record<string, boolean> = {};
|
||||
const updatedEmailValidity: Record<string, boolean> = {};
|
||||
|
||||
teamMembersToInvite?.forEach((member) => {
|
||||
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email);
|
||||
const roleValid = Boolean(member.role && member.role.trim() !== '');
|
||||
|
||||
if (!emailValid || !member.email) {
|
||||
isValid = false;
|
||||
setHasInvalidEmails(true);
|
||||
hasEmailErrors = true;
|
||||
}
|
||||
if (!roleValid) {
|
||||
isValid = false;
|
||||
hasRoleErrors = true;
|
||||
}
|
||||
|
||||
if (member.id) {
|
||||
updatedEmailValidity[member.id] = emailValid;
|
||||
}
|
||||
updatedValidity[member.id!] = emailValid;
|
||||
});
|
||||
|
||||
setEmailValidity(updatedValidity);
|
||||
setEmailValidity(updatedEmailValidity);
|
||||
setHasInvalidEmails(hasEmailErrors);
|
||||
setHasInvalidRoles(hasRoleErrors);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
@@ -126,10 +143,7 @@ function InviteTeamMembers({
|
||||
logEvent('Org Onboarding: Invite Team Members Failed', {
|
||||
teamMembers: teamMembersToInvite,
|
||||
});
|
||||
notifications.error({
|
||||
message: error.getErrorCode(),
|
||||
description: error.getErrorMessage(),
|
||||
});
|
||||
setInviteError(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -138,6 +152,8 @@ function InviteTeamMembers({
|
||||
if (validateAllUsers()) {
|
||||
setTeamMembers(teamMembersToInvite || []);
|
||||
setHasInvalidEmails(false);
|
||||
setHasInvalidRoles(false);
|
||||
setInviteError(null);
|
||||
sendInvites({
|
||||
invites: teamMembersToInvite || [],
|
||||
});
|
||||
@@ -146,37 +162,82 @@ function InviteTeamMembers({
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedValidateEmail = useCallback(
|
||||
debounce((email: string, memberId: string) => {
|
||||
debounce((email: string, memberId: string, updatedMembers: TeamMember[]) => {
|
||||
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
setEmailValidity((prev) => ({ ...prev, [memberId]: isValid }));
|
||||
|
||||
// Clear hasInvalidEmails only when ALL emails are valid
|
||||
if (hasInvalidEmails) {
|
||||
const allEmailsValid = updatedMembers.every(
|
||||
(m) => m.email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(m.email),
|
||||
);
|
||||
if (allEmailsValid) {
|
||||
setHasInvalidEmails(false);
|
||||
}
|
||||
}
|
||||
}, 500),
|
||||
[],
|
||||
[hasInvalidEmails],
|
||||
);
|
||||
|
||||
const handleEmailChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
member: TeamMember,
|
||||
): void => {
|
||||
const { value } = e.target;
|
||||
const updatedMembers = cloneDeep(teamMembersToInvite || []);
|
||||
const handleEmailChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>, member: TeamMember): void => {
|
||||
const { value } = e.target;
|
||||
const updatedMembers = cloneDeep(teamMembersToInvite || []);
|
||||
|
||||
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
|
||||
if (memberToUpdate) {
|
||||
memberToUpdate.email = value;
|
||||
setTeamMembersToInvite(updatedMembers);
|
||||
debouncedValidateEmail(value, member.id!);
|
||||
}
|
||||
};
|
||||
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
|
||||
if (memberToUpdate && member.id) {
|
||||
memberToUpdate.email = value;
|
||||
setTeamMembersToInvite(updatedMembers);
|
||||
debouncedValidateEmail(value, member.id, updatedMembers);
|
||||
// Clear API error when user starts typing
|
||||
if (inviteError) {
|
||||
setInviteError(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[debouncedValidateEmail, inviteError, teamMembersToInvite],
|
||||
);
|
||||
|
||||
const createEmailChangeHandler = useCallback(
|
||||
(member: TeamMember) => (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
handleEmailChange(e, member);
|
||||
},
|
||||
[handleEmailChange],
|
||||
);
|
||||
|
||||
const handleRoleChange = (role: string, member: TeamMember): void => {
|
||||
const updatedMembers = cloneDeep(teamMembersToInvite || []);
|
||||
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
|
||||
if (memberToUpdate) {
|
||||
if (memberToUpdate && member.id) {
|
||||
memberToUpdate.role = role;
|
||||
setTeamMembersToInvite(updatedMembers);
|
||||
|
||||
// Clear errors when user selects a role
|
||||
if (hasInvalidRoles) {
|
||||
// Check if all roles are now valid
|
||||
const allRolesValid = updatedMembers.every(
|
||||
(m) => m.role && m.role.trim() !== '',
|
||||
);
|
||||
if (allRolesValid) {
|
||||
setHasInvalidRoles(false);
|
||||
}
|
||||
}
|
||||
if (inviteError) {
|
||||
setInviteError(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getValidationErrorMessage = (): string => {
|
||||
if (hasInvalidEmails && hasInvalidRoles) {
|
||||
return 'Please enter valid emails and select roles for all team members';
|
||||
}
|
||||
if (hasInvalidEmails) {
|
||||
return 'Please enter valid emails for all team members';
|
||||
}
|
||||
return 'Please select roles for all team members';
|
||||
};
|
||||
|
||||
const handleDoLater = (): void => {
|
||||
logEvent('Org Onboarding: Clicked Do Later', {
|
||||
currentPageID: 4,
|
||||
@@ -185,122 +246,137 @@ function InviteTeamMembers({
|
||||
onNext();
|
||||
};
|
||||
|
||||
const isButtonDisabled = isSendingInvites || isLoading;
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
Invite your team members
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
The more your team uses SigNoz, the stronger your observability. Share
|
||||
dashboards, collaborate on alerts, and troubleshoot faster together.
|
||||
</Typography.Paragraph>
|
||||
<OnboardingQuestionHeader
|
||||
title="Invite your team"
|
||||
subtitle="SigNoz is a lot more useful with collaborators on board."
|
||||
/>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form invite-team-members-form">
|
||||
<div className="form-group">
|
||||
<div className="question-label">
|
||||
Collaborate with your team
|
||||
<div className="question-sub-label">
|
||||
Invite your team to the SigNoz workspace
|
||||
Invite your team to the SigNoz workspace
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-table">
|
||||
<div className="invite-team-members-table-header">
|
||||
<div className="table-header-cell email-header">Email address</div>
|
||||
<div className="table-header-cell role-header">Roles</div>
|
||||
<div className="table-header-cell action-header" />
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-container">
|
||||
{teamMembersToInvite?.map((member) => (
|
||||
<div className="team-member-row" key={member.id}>
|
||||
<div className="team-member-cell email-cell">
|
||||
<Input
|
||||
placeholder="e.g. john@signoz.io"
|
||||
value={member.email}
|
||||
type="email"
|
||||
id={`email-input-${member.id}`}
|
||||
name={`email-input-${member.id}`}
|
||||
required
|
||||
autoComplete="off"
|
||||
className="team-member-email-input"
|
||||
onChange={createEmailChangeHandler(member)}
|
||||
/>
|
||||
{member.id &&
|
||||
emailValidity[member.id] === false &&
|
||||
member.email.trim() !== '' && (
|
||||
<Typography.Text className="email-error-message">
|
||||
Invalid email address
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="team-member-cell role-cell">
|
||||
<Select
|
||||
value={member.role || undefined}
|
||||
onChange={(value): void => handleRoleChange(value, member)}
|
||||
className="team-member-role-select"
|
||||
placeholder="Select roles"
|
||||
suffixIcon={<ChevronDown size={14} />}
|
||||
>
|
||||
<Select.Option value="VIEWER">Viewer</Select.Option>
|
||||
<Select.Option value="EDITOR">Editor</Select.Option>
|
||||
<Select.Option value="ADMIN">Admin</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="team-member-cell action-cell">
|
||||
{teamMembersToInvite && teamMembersToInvite.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="remove-team-member-button"
|
||||
onClick={(): void => handleRemoveTeamMember(member.id)}
|
||||
aria-label="Remove team member"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-add-another-member-container">
|
||||
<Button
|
||||
variant="dashed"
|
||||
color="secondary"
|
||||
className="add-another-member-button"
|
||||
prefixIcon={<Plus size={12} />}
|
||||
onClick={handleAddTeamMember}
|
||||
>
|
||||
Add another
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-container">
|
||||
{teamMembersToInvite?.map((member) => (
|
||||
<div className="team-member-container" key={member.id}>
|
||||
<Input
|
||||
placeholder="your-teammate@org.com"
|
||||
value={member.email}
|
||||
type="email"
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
className="team-member-email-input"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
|
||||
handleEmailChange(e, member)
|
||||
}
|
||||
addonAfter={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
emailValidity[member.id!] === undefined ? null : emailValidity[
|
||||
member.id!
|
||||
] ? (
|
||||
<CheckCircle size={14} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={14} color={Color.BG_SIENNA_500} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
defaultValue={member.role}
|
||||
onChange={(value): void => handleRoleChange(value, member)}
|
||||
className="team-member-role-select"
|
||||
>
|
||||
<Select.Option value="VIEWER">Viewer</Select.Option>
|
||||
<Select.Option value="EDITOR">Editor</Select.Option>
|
||||
<Select.Option value="ADMIN">Admin</Select.Option>
|
||||
</Select>
|
||||
|
||||
{teamMembersToInvite?.length > 1 && (
|
||||
<Button
|
||||
type="primary"
|
||||
className="remove-team-member-button"
|
||||
icon={<X size={14} />}
|
||||
onClick={(): void => handleRemoveTeamMember(member.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-add-another-member-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className="add-another-member-button"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={handleAddTeamMember}
|
||||
>
|
||||
Member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasInvalidEmails && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> Please enter valid emails for all team
|
||||
members
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button type="default" className="next-button" onClick={onBack}>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
{(hasInvalidEmails || hasInvalidRoles) && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className="invite-team-members-error-callout"
|
||||
description={getValidationErrorMessage()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inviteError && !hasInvalidEmails && !hasInvalidRoles && (
|
||||
<AuthError error={inviteError} />
|
||||
)}
|
||||
|
||||
<div className="onboarding-buttons-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className="next-button"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={`onboarding-next-button ${isButtonDisabled ? 'disabled' : ''}`}
|
||||
onClick={handleNext}
|
||||
loading={isSendingInvites || isLoading}
|
||||
disabled={isButtonDisabled}
|
||||
suffixIcon={
|
||||
isButtonDisabled ? (
|
||||
<Loader2 className="animate-spin" size={12} />
|
||||
) : (
|
||||
<ArrowRight size={12} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Send Invites
|
||||
<ArrowRight size={14} />
|
||||
Complete
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="do-later-container">
|
||||
<Button
|
||||
type="link"
|
||||
className="do-later-button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="onboarding-do-later-button"
|
||||
onClick={handleDoLater}
|
||||
disabled={isSendingInvites}
|
||||
disabled={isButtonDisabled}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin" size={16} />}
|
||||
|
||||
<span>I'll do this later</span>
|
||||
I'll do this later
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Typography } from 'antd';
|
||||
|
||||
interface OnboardingQuestionHeaderProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export function OnboardingQuestionHeader({
|
||||
title,
|
||||
subtitle,
|
||||
}: OnboardingQuestionHeaderProps): JSX.Element {
|
||||
return (
|
||||
<div className="onboarding-header-section">
|
||||
<div className="onboarding-header-icon">
|
||||
<img src="/svgs/barber-pool.svg" alt="SigNoz" width="32" height="32" />
|
||||
</div>
|
||||
<Typography.Title level={4} className="onboarding-header-title">
|
||||
{title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="onboarding-header-subtitle">
|
||||
{subtitle}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { OnboardingQuestionHeader } from './OnboardingQuestionHeader';
|
||||
@@ -4,36 +4,67 @@
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-width: 1176px;
|
||||
|
||||
.onboarding-questionaire-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 56px;
|
||||
}
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
|
||||
.onboarding-questionaire-content {
|
||||
height: calc(100vh - 56px - 60px);
|
||||
width: 100%;
|
||||
max-width: 576px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
|
||||
.questions-container {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
font-family: Inter;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
max-width: 576px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.onboarding-header-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
|
||||
.onboarding-header-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.onboarding-header-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onboarding-header-subtitle {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
max-width: 528px;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -54,22 +85,22 @@
|
||||
}
|
||||
|
||||
.questions-form-container {
|
||||
max-width: 600px;
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.questions-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
min-height: 420px;
|
||||
padding: 20px 24px 24px 24px;
|
||||
padding: 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
@@ -86,43 +117,36 @@
|
||||
|
||||
.discover-signoz-input {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
height: 80px;
|
||||
resize: none;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
border-radius: 2px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
padding: 6px 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-vanilla-400);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-team-members-form {
|
||||
min-height: calc(420px - 24px);
|
||||
max-height: calc(420px - 24px);
|
||||
padding-right: 12px;
|
||||
|
||||
.invite-team-members-container {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.form-group {
|
||||
gap: 24px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,30 +182,106 @@
|
||||
}
|
||||
|
||||
.question-label {
|
||||
color: var(--bg-vanilla-100);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
|
||||
.question-sub-label {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.next-prev-container {
|
||||
.onboarding-buttons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
flex: 1;
|
||||
.onboarding-back-button {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-next-button {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--semantic-primary-background, #4e74f8);
|
||||
border: none;
|
||||
color: var(--semantic-primary-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-do-later-button {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,15 +289,38 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
width: 100%;
|
||||
width: calc(100% - 16px);
|
||||
|
||||
.ant-slider .ant-slider-mark {
|
||||
font-size: 10px;
|
||||
margin-top: 12px;
|
||||
|
||||
.ant-slider-mark-text {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.logs-slider-container {
|
||||
.ant-slider .ant-slider-mark {
|
||||
.ant-slider-mark-text {
|
||||
&:last-child {
|
||||
left: calc(100% - 8px) !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,29 +342,57 @@
|
||||
}
|
||||
|
||||
.question {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-family: Inter, sans-serif;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.065px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.question-slider {
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.065px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,34 +442,170 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.observability-tools-checkbox-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 12px;
|
||||
width: 528px;
|
||||
align-items: flex-start;
|
||||
|
||||
.observability-tool-checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
width: calc((528px - 12px) / 2);
|
||||
min-width: 258px;
|
||||
flex: 0 0 calc((528px - 12px) / 2);
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
&.checkbox-item {
|
||||
width: calc((528px - 12px) / 2) !important;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.observability-tool-other-input {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.opentelemetry-radio-container {
|
||||
width: 528px;
|
||||
|
||||
.opentelemetry-radio-group {
|
||||
width: 100%;
|
||||
|
||||
.opentelemetry-radio-items-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.opentelemetry-radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
width: calc((528px - 12px) / 2);
|
||||
min-width: 258px;
|
||||
flex: 0 0 calc((528px - 12px) / 2);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ant-radio {
|
||||
.ant-radio-inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
gap: 0;
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
|
||||
&.checkbox-item-others {
|
||||
.onboarding-questionaire-other-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 0 0;
|
||||
height: 32px;
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
letter-spacing: -0.065px;
|
||||
font-variant-numeric: slashed-zero;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
.ant-checkbox {
|
||||
.ant-checkbox-inner {
|
||||
border-color: var(--bg-slate-100);
|
||||
background-color: var(--bg-ink-200);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1.5px solid var(--levels-l3-background, #23262e);
|
||||
border-radius: 2px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--levels-l1-foreground, #eceef2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,8 +646,14 @@
|
||||
|
||||
.add-another-member-button,
|
||||
.remove-team-member-button {
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
text-align: center;
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.remove-team-member-button {
|
||||
@@ -402,26 +695,6 @@
|
||||
min-width: 258px;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding: 8px 12px 8px 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: stretch;
|
||||
border: 0px;
|
||||
border-radius: 50px;
|
||||
margin-top: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.next-button.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 18px;
|
||||
color: var(--bg-vanilla-100);
|
||||
@@ -440,7 +713,7 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,25 +734,24 @@
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--bg-slate-300) !important;
|
||||
.onboarding-header-title {
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: var(--bg-slate-400) !important;
|
||||
.onboarding-header-subtitle {
|
||||
color: var(--semantic-secondary-foreground, #747b8b) !important;
|
||||
}
|
||||
|
||||
.questions-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
min-height: 420px;
|
||||
padding: 20px 24px 24px 24px;
|
||||
padding: 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--semantic-secondary-border, #e9e9e9);
|
||||
background: var(--semantic-secondary-background, #ffffff);
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
@@ -495,35 +767,18 @@
|
||||
}
|
||||
|
||||
.discover-signoz-input {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
font-weight: 400;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-slate-400);
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-team-members-form {
|
||||
.invite-team-members-container {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&:focus-visible {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,36 +807,86 @@
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.question-sub-label {
|
||||
color: var(--bg-slate-400);
|
||||
.question {
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
|
||||
.question {
|
||||
color: var(--bg-slate-300);
|
||||
.question-slider {
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
label {
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
color: var(--bg-ink-300);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
.ant-checkbox {
|
||||
.ant-checkbox-inner {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: var(--levels-l3-background, #ffffff);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: var(--bg-robin-500);
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--levels-l1-foreground, #1a1d26) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.checkbox-item-others {
|
||||
.onboarding-questionaire-other-input {
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.observability-tool-others-item {
|
||||
.onboarding-questionaire-other-input {
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
border: 1px solid var(--levels-l3-border, #e9e9e9);
|
||||
background: var(--levels-l3-background, #ffffff);
|
||||
color: var(--levels-l3-foreground, #1a1d26);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button,
|
||||
@@ -671,6 +976,36 @@
|
||||
.arrow {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.opentelemetry-radio-container {
|
||||
.opentelemetry-radio-group {
|
||||
.opentelemetry-radio-items-wrapper {
|
||||
.opentelemetry-radio-item {
|
||||
color: var(--levels-l1-foreground, #1a1d26);
|
||||
|
||||
.ant-radio {
|
||||
.ant-radio-inner {
|
||||
border-color: var(--levels-l3-border, #e9e9e9);
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
background-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-back-button {
|
||||
border-color: var(--semantic-secondary-border, #e9e9e9);
|
||||
color: var(--semantic-secondary-foreground, #747b8b);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Button, Slider, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Slider, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, Loader2, Minus } from 'lucide-react';
|
||||
import { ArrowRight, Loader2, Minus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
|
||||
|
||||
export interface OptimiseSignozDetails {
|
||||
logsPerDay: number;
|
||||
hostsPerDay: number;
|
||||
@@ -47,7 +50,6 @@ interface OptimiseSignozNeedsProps {
|
||||
optimiseSignozDetails: OptimiseSignozDetails;
|
||||
setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
onWillDoLater: () => void;
|
||||
isUpdatingProfile: boolean;
|
||||
isNextDisabled: boolean;
|
||||
@@ -82,7 +84,6 @@ function OptimiseSignozNeeds({
|
||||
optimiseSignozDetails,
|
||||
setOptimiseSignozDetails,
|
||||
onNext,
|
||||
onBack,
|
||||
onWillDoLater,
|
||||
isNextDisabled,
|
||||
}: OptimiseSignozNeedsProps): JSX.Element {
|
||||
@@ -131,10 +132,6 @@ function OptimiseSignozNeeds({
|
||||
onNext();
|
||||
};
|
||||
|
||||
const handleOnBack = (): void => {
|
||||
onBack();
|
||||
};
|
||||
|
||||
const handleWillDoLater = (): void => {
|
||||
setOptimiseSignozDetails({
|
||||
logsPerDay: 0,
|
||||
@@ -189,24 +186,24 @@ function OptimiseSignozNeeds({
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
Optimize SigNoz for Your Needs
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
Give us a quick sense of your scale so SigNoz can keep up!
|
||||
</Typography.Paragraph>
|
||||
<OnboardingQuestionHeader
|
||||
title="Set up your workspace"
|
||||
subtitle="Tailor SigNoz to suit your observability needs."
|
||||
/>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
<Typography.Paragraph className="question">
|
||||
What does your scale approximately look like?
|
||||
</Typography.Paragraph>
|
||||
<div className="form-group">
|
||||
<Typography.Paragraph className="question">
|
||||
What does your scale approximately look like?
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
<label className="question-slider" htmlFor="organisationName">
|
||||
Logs / Day
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
<div className="slider-container logs-slider-container">
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
@@ -230,7 +227,7 @@ function OptimiseSignozNeeds({
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
<label className="question-slider" htmlFor="organisationName">
|
||||
Metrics <Minus size={14} /> Number of Hosts
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
@@ -257,7 +254,7 @@ function OptimiseSignozNeeds({
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
<label className="question-slider" htmlFor="organisationName">
|
||||
Number of services
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
@@ -284,34 +281,32 @@ function OptimiseSignozNeeds({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<div className="onboarding-buttons-container">
|
||||
<Button
|
||||
type="default"
|
||||
className="next-button"
|
||||
onClick={handleOnBack}
|
||||
disabled={isUpdatingProfile}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className="next-button"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={`onboarding-next-button ${
|
||||
isUpdatingProfile || isNextDisabled ? 'disabled' : ''
|
||||
}`}
|
||||
onClick={handleOnNext}
|
||||
disabled={isUpdatingProfile || isNextDisabled}
|
||||
suffixIcon={
|
||||
isUpdatingProfile ? (
|
||||
<Loader2 className="animate-spin" size={12} />
|
||||
) : (
|
||||
<ArrowRight size={12} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Next{' '}
|
||||
{isUpdatingProfile ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ArrowRight size={14} />
|
||||
)}
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="do-later-container">
|
||||
<Button type="link" onClick={handleWillDoLater}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="onboarding-do-later-button"
|
||||
onClick={handleWillDoLater}
|
||||
disabled={isUpdatingProfile}
|
||||
>
|
||||
I'll do this later
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import '../OnboardingQuestionaire.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Radio, Typography } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/es/radio';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import editOrg from 'api/organization/editOrg';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -39,6 +42,7 @@ const observabilityTools = {
|
||||
GCPNativeO11yTools: 'GCP-native o11y tools',
|
||||
Honeycomb: 'Honeycomb',
|
||||
None: 'None/Starting fresh',
|
||||
Others: 'Others',
|
||||
};
|
||||
|
||||
function OrgQuestions({
|
||||
@@ -46,7 +50,7 @@ function OrgQuestions({
|
||||
orgDetails,
|
||||
onNext,
|
||||
}: OrgQuestionsProps): JSX.Element {
|
||||
const { user, updateOrg } = useAppContext();
|
||||
const { updateOrg } = useAppContext();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
@@ -68,11 +72,12 @@ function OrgQuestions({
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [usesOtel, setUsesOtel] = useState<boolean | null>(
|
||||
orgDetails?.usesOtel || null,
|
||||
);
|
||||
const [usesOtel, setUsesOtel] = useState<boolean | null>(orgDetails.usesOtel);
|
||||
|
||||
const handleOrgNameUpdate = async (): Promise<void> => {
|
||||
const usesObservability =
|
||||
!observabilityTool?.includes('None') && observabilityTool !== null;
|
||||
|
||||
/* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */
|
||||
if (
|
||||
!currentOrgData ||
|
||||
@@ -81,7 +86,7 @@ function OrgQuestions({
|
||||
orgDetails.organisationName === organisationName
|
||||
) {
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
usesObservability: !observabilityTool?.includes('None'),
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
@@ -89,7 +94,7 @@ function OrgQuestions({
|
||||
|
||||
onNext({
|
||||
organisationName,
|
||||
usesObservability: !observabilityTool?.includes('None'),
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
@@ -112,7 +117,7 @@ function OrgQuestions({
|
||||
});
|
||||
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
usesObservability: !observabilityTool?.includes('None'),
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
@@ -120,7 +125,7 @@ function OrgQuestions({
|
||||
|
||||
onNext({
|
||||
organisationName,
|
||||
usesObservability: !observabilityTool?.includes('None'),
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
@@ -177,31 +182,47 @@ function OrgQuestions({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [organisationName, usesOtel, observabilityTool, otherTool]);
|
||||
|
||||
const createObservabilityToolHandler = (tool: string) => (
|
||||
checked: boolean,
|
||||
): void => {
|
||||
if (checked) {
|
||||
setObservabilityTool(tool);
|
||||
} else if (observabilityTool === tool) {
|
||||
setObservabilityTool(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtelChange = (value: string): void => {
|
||||
setUsesOtel(value === 'yes');
|
||||
};
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
handleOrgNameUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
{user?.displayName ? `Welcome, ${user.displayName}!` : 'Welcome!'}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
We'll help you get the most out of SigNoz, whether you're new to
|
||||
observability or a seasoned pro.
|
||||
</Typography.Paragraph>
|
||||
<div className="onboarding-header-section">
|
||||
<div className="onboarding-header-icon">🎉</div>
|
||||
<Typography.Title level={4} className="onboarding-header-title">
|
||||
Welcome to SigNoz Cloud
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="onboarding-header-subtitle">
|
||||
Let's get you started
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
Your Organisation Name
|
||||
Name of your company
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
name="organisationName"
|
||||
id="organisationName"
|
||||
placeholder="For eg. Simpsonville..."
|
||||
placeholder="e.g. Simpsonville"
|
||||
autoComplete="off"
|
||||
value={organisationName}
|
||||
onChange={(e): void => setOrganisationName(e.target.value)}
|
||||
@@ -212,105 +233,93 @@ function OrgQuestions({
|
||||
<label className="question" htmlFor="observabilityTool">
|
||||
Which observability tool do you currently use?
|
||||
</label>
|
||||
<div className="two-column-grid">
|
||||
{Object.keys(observabilityTools).map((tool) => (
|
||||
<Button
|
||||
key={tool}
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
observabilityTool === tool ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setObservabilityTool(tool)}
|
||||
>
|
||||
{observabilityTools[tool as keyof typeof observabilityTools]}
|
||||
|
||||
{observabilityTool === tool && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{observabilityTool === 'Others' ? (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="Please specify the tool"
|
||||
value={otherTool || ''}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherTool && otherTool !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onChange={(e): void => setOtherTool(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
observabilityTool === 'Others' ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setObservabilityTool('Others')}
|
||||
>
|
||||
Others
|
||||
</Button>
|
||||
)}
|
||||
<div className="observability-tools-checkbox-container">
|
||||
{Object.entries(observabilityTools).map(([tool, label]) => {
|
||||
if (tool === 'Others') {
|
||||
return (
|
||||
<div
|
||||
key={tool}
|
||||
className="checkbox-item observability-tool-checkbox-item observability-tool-others-item"
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${tool}`}
|
||||
checked={observabilityTool === tool}
|
||||
onCheckedChange={createObservabilityToolHandler(tool)}
|
||||
labelName={observabilityTool === 'Others' ? '' : label}
|
||||
/>
|
||||
{observabilityTool === 'Others' && (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="What tool do you currently use?"
|
||||
value={otherTool || ''}
|
||||
autoFocus
|
||||
onChange={(e): void => setOtherTool(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={tool}
|
||||
className="checkbox-item observability-tool-checkbox-item"
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${tool}`}
|
||||
checked={observabilityTool === tool}
|
||||
onCheckedChange={createObservabilityToolHandler(tool)}
|
||||
labelName={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="question">Do you already use OpenTelemetry?</div>
|
||||
<div className="two-column-grid">
|
||||
<Button
|
||||
type="primary"
|
||||
name="usesObservability"
|
||||
className={`onboarding-questionaire-button ${
|
||||
usesOtel === true ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => {
|
||||
setUsesOtel(true);
|
||||
}}
|
||||
<div className="opentelemetry-radio-container">
|
||||
<Radio.Group
|
||||
value={((): string | undefined => {
|
||||
if (usesOtel === true) return 'yes';
|
||||
if (usesOtel === false) return 'no';
|
||||
return undefined;
|
||||
})()}
|
||||
onChange={(e: RadioChangeEvent): void =>
|
||||
handleOtelChange(e.target.value)
|
||||
}
|
||||
className="opentelemetry-radio-group"
|
||||
>
|
||||
Yes{' '}
|
||||
{usesOtel === true && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
usesOtel === false ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => {
|
||||
setUsesOtel(false);
|
||||
}}
|
||||
>
|
||||
No{' '}
|
||||
{usesOtel === false && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
<div className="opentelemetry-radio-items-wrapper">
|
||||
<Radio value="yes" className="opentelemetry-radio-item">
|
||||
Yes
|
||||
</Radio>
|
||||
<Radio value="no" className="opentelemetry-radio-item">
|
||||
No
|
||||
</Radio>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className={`next-button ${isNextDisabled ? 'disabled' : ''}`}
|
||||
onClick={handleOnNext}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
Next
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={`onboarding-next-button ${isNextDisabled ? 'disabled' : ''}`}
|
||||
onClick={handleOnNext}
|
||||
disabled={isNextDisabled}
|
||||
suffixIcon={
|
||||
isLoading ? (
|
||||
<Loader2 className="animate-spin" size={12} />
|
||||
) : (
|
||||
<ArrowRight size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ArrowRight size={12} />
|
||||
)
|
||||
}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import OnboardingQuestionaire from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
pathname: '/onboarding',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// API Endpoints
|
||||
const ORG_PREFERENCES_ENDPOINT = '*/api/v1/org/preferences/list';
|
||||
const UPDATE_ORG_PREFERENCE_ENDPOINT = '*/api/v1/org/preferences/name/update';
|
||||
const UPDATE_PROFILE_ENDPOINT = '*/api/gateway/v2/profiles/me';
|
||||
const EDIT_ORG_ENDPOINT = '*/api/v2/orgs/me';
|
||||
const INVITE_USERS_ENDPOINT = '*/api/v1/invite/bulk/create';
|
||||
|
||||
const mockOrgPreferences = {
|
||||
data: {
|
||||
org_onboarding: false,
|
||||
},
|
||||
status: 'success',
|
||||
};
|
||||
|
||||
describe('OnboardingQuestionaire Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
server.use(
|
||||
rest.get(ORG_PREFERENCES_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockOrgPreferences)),
|
||||
),
|
||||
rest.put(EDIT_ORG_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(204), ctx.json({ status: 'success' })),
|
||||
),
|
||||
rest.put(UPDATE_PROFILE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
|
||||
),
|
||||
rest.post(UPDATE_ORG_PREFERENCE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
rest.post(INVITE_USERS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success' })),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Step 1: Organization Details', () => {
|
||||
it('renders organization questions on initial load', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
expect(screen.getByText(/welcome to signoz cloud/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/name of your company/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/which observability tool do you currently use/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables next button when required fields are empty', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables next button when all required fields are filled', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
const datadogCheckbox = screen.getByLabelText(/datadog/i);
|
||||
await user.click(datadogCheckbox);
|
||||
|
||||
const otelYes = screen.getByRole('radio', { name: /yes/i });
|
||||
await user.click(otelYes);
|
||||
|
||||
const nextButton = await screen.findByRole('button', { name: /next/i });
|
||||
expect(nextButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows other tool input when Others is selected', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const othersCheckbox = screen.getByLabelText(/^others$/i);
|
||||
await user.click(othersCheckbox);
|
||||
|
||||
expect(
|
||||
await screen.findByPlaceholderText(/what tool do you currently use/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('proceeds to step 2 when next is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
||||
await user.click(nextButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/how did you first come across signoz/i, {}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step 2: About SigNoz', () => {
|
||||
it('renders about signoz questions after step 1 completion', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(/set up your workspace/i, {}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/how did you first come across signoz/i, {}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables next button when fields are empty', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('enables next button when all fields are filled', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByPlaceholderText(/e\.g\., googling/i, {}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const discoverInput = screen.getByPlaceholderText(/e\.g\., googling/i);
|
||||
await user.type(discoverInput, 'Found via Google search');
|
||||
|
||||
const interestCheckbox = screen.getByLabelText(
|
||||
/lowering observability costs/i,
|
||||
);
|
||||
await user.click(interestCheckbox);
|
||||
|
||||
const nextButton = await screen.findByRole('button', { name: /next/i });
|
||||
expect(nextButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows other interest input when Others checkbox is selected', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(/what got you interested in signoz/i, {}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const othersCheckbox = screen.getByLabelText(/^others$/i);
|
||||
await user.click(othersCheckbox);
|
||||
|
||||
expect(
|
||||
await screen.findByPlaceholderText(
|
||||
/what got you interested in signoz/i,
|
||||
{},
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step 3: Optimize SigNoz Needs', () => {
|
||||
it('renders scale questions after step 2 completion', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate through steps 1 and 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByPlaceholderText(/e\.g\., googling/i, {}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.type(
|
||||
screen.getByPlaceholderText(/e\.g\., googling/i),
|
||||
'Found via Google',
|
||||
);
|
||||
await user.click(screen.getByLabelText(/lowering observability costs/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/what does your scale approximately look like/i,
|
||||
{},
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText(/logs \/ day/i, {})).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/number of services/i, {}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows do later button', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 3
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByPlaceholderText(/e\.g\., googling/i, {}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.type(
|
||||
screen.getByPlaceholderText(/e\.g\., googling/i),
|
||||
'Found via Google',
|
||||
);
|
||||
await user.click(screen.getByLabelText(/lowering observability costs/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /i'll do this later/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('handles organization update error gracefully', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.put(EDIT_ORG_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(500),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Failed to update organization',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
||||
await user.click(nextButton);
|
||||
|
||||
// Component should still be functional
|
||||
await waitFor(() => {
|
||||
expect(nextButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
SignozDetails,
|
||||
} from './AboutSigNozQuestions/AboutSigNozQuestions';
|
||||
import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers';
|
||||
import { OnboardingHeader } from './OnboardingHeader/OnboardingHeader';
|
||||
import OptimiseSignozNeeds, {
|
||||
OptimiseSignozDetails,
|
||||
} from './OptimiseSignozNeeds/OptimiseSignozNeeds';
|
||||
@@ -57,7 +56,6 @@ const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = {
|
||||
services: 0,
|
||||
};
|
||||
|
||||
const BACK_BUTTON_EVENT_NAME = 'Org Onboarding: Back Button Clicked';
|
||||
const NEXT_BUTTON_EVENT_NAME = 'Org Onboarding: Next Button Clicked';
|
||||
const ONBOARDING_COMPLETE_EVENT_NAME = 'Org Onboarding: Complete';
|
||||
|
||||
@@ -207,15 +205,14 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="onboarding-questionaire-container">
|
||||
<div className="onboarding-questionaire-header">
|
||||
<OnboardingHeader />
|
||||
</div>
|
||||
|
||||
<div className="onboarding-questionaire-content">
|
||||
{currentStep === 1 && (
|
||||
<OrgQuestions
|
||||
currentOrgData={currentOrgData}
|
||||
orgDetails={orgDetails}
|
||||
orgDetails={{
|
||||
...orgDetails,
|
||||
usesOtel: orgDetails.usesOtel ?? null,
|
||||
}}
|
||||
onNext={(orgDetails: OrgDetails): void => {
|
||||
logEvent(NEXT_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 1,
|
||||
@@ -232,13 +229,6 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
<AboutSigNozQuestions
|
||||
signozDetails={signozDetails}
|
||||
setSignozDetails={setSignozDetails}
|
||||
onBack={(): void => {
|
||||
logEvent(BACK_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 2,
|
||||
prevPageID: 1,
|
||||
});
|
||||
setCurrentStep(1);
|
||||
}}
|
||||
onNext={(): void => {
|
||||
logEvent(NEXT_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 2,
|
||||
@@ -255,13 +245,6 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
isUpdatingProfile={isUpdatingProfile}
|
||||
optimiseSignozDetails={optimiseSignozDetails}
|
||||
setOptimiseSignozDetails={setOptimiseSignozDetails}
|
||||
onBack={(): void => {
|
||||
logEvent(BACK_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 3,
|
||||
prevPageID: 2,
|
||||
});
|
||||
setCurrentStep(2);
|
||||
}}
|
||||
onNext={handleUpdateProfile}
|
||||
onWillDoLater={handleUpdateProfile}
|
||||
/>
|
||||
@@ -272,13 +255,6 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
isLoading={updatingOrgOnboardingStatus}
|
||||
teamMembers={teamMembers}
|
||||
setTeamMembers={setTeamMembers}
|
||||
onBack={(): void => {
|
||||
logEvent(BACK_BUTTON_EVENT_NAME, {
|
||||
currentPageID: 4,
|
||||
prevPageID: 3,
|
||||
});
|
||||
setCurrentStep(3);
|
||||
}}
|
||||
onNext={handleOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
212
frontend/src/container/ResetPassword/ResetPassword.styles.scss
Normal file
212
frontend/src/container/ResetPassword/ResetPassword.styles.scss
Normal file
@@ -0,0 +1,212 @@
|
||||
.reset-password-card {
|
||||
width: 576px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.reset-password-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
|
||||
.reset-password-header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
color: var(--semantic-primary-foreground);
|
||||
}
|
||||
|
||||
.reset-password-header-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.reset-password-header-subtitle {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.reset-password-version-badge {
|
||||
margin-top: 8px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
color: var(--semantic-secondary-foreground);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-password-form {
|
||||
width: 100%;
|
||||
|
||||
.reset-password-form-container {
|
||||
width: 100%;
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
|
||||
.reset-password-form-fields {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.reset-password-field-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.reset-password-form-input {
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
|
||||
&.ant-input,
|
||||
&.ant-input-password,
|
||||
&.ant-input-affix-wrapper {
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
.ant-input {
|
||||
height: auto;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reset-password-error-callout {
|
||||
margin-top: 24px;
|
||||
background: rgba(229, 72, 77, 0.1);
|
||||
border: 1px solid rgba(229, 72, 77, 0.2);
|
||||
border-radius: 4px;
|
||||
animation: horizontal-shaking 300ms ease-out;
|
||||
}
|
||||
|
||||
.reset-password-form-actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.reset-password-submit-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.reset-password-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.reset-password-card {
|
||||
.reset-password-header {
|
||||
.reset-password-header-icon {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
.reset-password-header-title {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
.reset-password-header-subtitle {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.reset-password-version-badge {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-password-form {
|
||||
.reset-password-form-container {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
|
||||
|
||||
.reset-password-form-input {
|
||||
&.ant-input,
|
||||
&.ant-input-password,
|
||||
&.ant-input-affix-wrapper {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
.ant-input {
|
||||
background: transparent;
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes horizontal-shaking {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import ResetPassword from './index';
|
||||
|
||||
jest.mock('api/v1/factor_password/resetPassword', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('ResetPassword Component', () => {
|
||||
beforeEach(() => {
|
||||
userEvent.setup();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders ResetPassword component correctly', () => {
|
||||
render(<ResetPassword version="1.0" />);
|
||||
expect(screen.getByText('Reset Your Password')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
|
||||
expect(
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
screen.getByRole('button', { name: 'Get Started' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the "Get Started" button when password is invalid', async () => {
|
||||
render(<ResetPassword version="1.0" />);
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
||||
const submitButton = screen.getByRole('button', { name: 'Get Started' });
|
||||
|
||||
act(() => {
|
||||
// Set invalid password
|
||||
fireEvent.change(passwordInput, { target: { value: 'password' } });
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: 'password' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Expect the "Get Started" button to be disabled
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('enables the "Get Started" button when password is valid', async () => {
|
||||
render(<ResetPassword version="1.0" />);
|
||||
|
||||
const passwordInput = screen.getByLabelText('Password');
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
|
||||
const submitButton = screen.getByRole('button', { name: 'Get Started' });
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(passwordInput, { target: { value: 'newPassword' } });
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: 'newPassword' } });
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Expect the "Get Started" button to be enabled
|
||||
expect(submitButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,357 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { Logout } from 'api/utils';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import ResetPassword from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '?token=reset-token-123',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('api/utils', () => ({
|
||||
Logout: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockSuccessNotification = jest.fn();
|
||||
const mockErrorNotification = jest.fn();
|
||||
|
||||
interface MockNotifications {
|
||||
success: jest.MockedFunction<(...args: unknown[]) => void>;
|
||||
error: jest.MockedFunction<(...args: unknown[]) => void>;
|
||||
}
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): { notifications: MockNotifications } => ({
|
||||
notifications: {
|
||||
success: mockSuccessNotification,
|
||||
error: mockErrorNotification,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const RESET_PASSWORD_ENDPOINT = '*/resetPassword';
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
>;
|
||||
|
||||
describe('ResetPassword Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSuccessNotification.mockClear();
|
||||
mockErrorNotification.mockClear();
|
||||
window.history.pushState({}, '', '/password-reset?token=reset-token-123');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Initial Render', () => {
|
||||
it('renders reset password form with all required fields', () => {
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
expect(screen.getByText(/reset your password/i)).toBeInTheDocument();
|
||||
expect(screen.getByTestId('password')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('confirmPassword')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /reset password/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/signoz 1\.0\.0/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to login when token is missing', () => {
|
||||
window.history.pushState({}, '', '/password-reset');
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset',
|
||||
});
|
||||
|
||||
expect(Logout).toHaveBeenCalled();
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('disables submit button when passwords do not match', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password456');
|
||||
await user.tab(); // Blur the confirm password field to trigger validation
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/passwords don't match/i)).toBeInTheDocument();
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('enables submit button when passwords match', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'newPassword123');
|
||||
await user.type(confirmPasswordInput, 'newPassword123');
|
||||
|
||||
// Wait for debounced validation
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
it('clears password mismatch error when passwords match', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password456');
|
||||
await user.tab(); // Blur the confirm password field to trigger validation
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/passwords don't match/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.clear(confirmPasswordInput);
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
await user.tab(); // Blur again to trigger validation
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
screen.queryByText(/passwords don't match/i),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful Password Reset', () => {
|
||||
it('successfully resets password and redirects to login', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(RESET_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
message: 'Password reset successfully',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'newPassword123');
|
||||
await user.type(confirmPasswordInput, 'newPassword123');
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSuccessNotification).toHaveBeenCalled();
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.LOGIN);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('displays error message when reset password API fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(RESET_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INVALID_TOKEN',
|
||||
message: 'Invalid or expired reset token',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=invalid-token',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'newPassword123');
|
||||
await user.type(confirmPasswordInput, 'newPassword123');
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/invalid or expired reset token/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show API error when password mismatch error is shown', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(RESET_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INVALID_TOKEN',
|
||||
message: 'Invalid token',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password456');
|
||||
await user.tab(); // Blur the confirm password field to trigger validation
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/passwords don't match/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/invalid token/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
it('disables submit button during password reset', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(RESET_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(100),
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
message: 'Password reset successfully',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<ResetPassword version="1.0.0" />, undefined, {
|
||||
initialRoute: '/password-reset?token=reset-token-123',
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /reset password/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'newPassword123');
|
||||
await user.type(confirmPasswordInput, 'newPassword123');
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
},
|
||||
{ timeout: 200 },
|
||||
);
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
// Button should be disabled during API call
|
||||
await waitFor(() => {
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,24 @@
|
||||
import { Button, Form, Input, Typography } from 'antd';
|
||||
import './ResetPassword.styles.scss';
|
||||
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Form, Input as AntdInput, Typography } from 'antd';
|
||||
import { Logout } from 'api/utils';
|
||||
import resetPasswordApi from 'api/v1/factor_password/resetPassword';
|
||||
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, CircleAlert, KeyRound } from 'lucide-react';
|
||||
import { Label } from 'pages/SignUp/styles';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-use';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { ButtonContainer, FormContainer, FormWrapper } from './styles';
|
||||
|
||||
const { Title } = Typography;
|
||||
import { FormContainer } from './styles';
|
||||
|
||||
type FormValues = { password: string; confirmPassword: string };
|
||||
|
||||
@@ -23,6 +27,8 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
false,
|
||||
);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<APIError | null>();
|
||||
|
||||
const [isValidPassword, setIsValidPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation(['common']);
|
||||
@@ -42,6 +48,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
const handleFormSubmit: () => Promise<void> = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
const { password } = form.getFieldsValue();
|
||||
|
||||
await resetPasswordApi({
|
||||
@@ -59,10 +66,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
setErrorMessage(error as APIError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,6 +94,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
setIsValidPassword(false);
|
||||
}
|
||||
|
||||
// Only clear error if passwords match while typing (but don't set error until blur)
|
||||
if (
|
||||
password &&
|
||||
confirmPassword &&
|
||||
@@ -97,11 +102,38 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
confirmPassword.trim()
|
||||
) {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
|
||||
// Only clear error if passwords match, don't set error on mismatch
|
||||
if (isValid) {
|
||||
setConfirmPasswordError(false);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const handlePasswordBlur = (): void => {
|
||||
const { confirmPassword } = form.getFieldsValue();
|
||||
// Only validate if confirm password has a value
|
||||
if (confirmPassword && confirmPassword.trim()) {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
setConfirmPasswordError(!isValid);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleConfirmPasswordBlur = (): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
if (
|
||||
password &&
|
||||
password.trim() &&
|
||||
confirmPassword &&
|
||||
confirmPassword.trim()
|
||||
) {
|
||||
const isValid = validatePassword();
|
||||
setIsValidPassword(isValid);
|
||||
setConfirmPasswordError(!isValid);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
const isValid = validatePassword();
|
||||
@@ -113,69 +145,100 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<WelcomeLeftContainer version={version}>
|
||||
<FormWrapper>
|
||||
<FormContainer form={form} onFinish={handleSubmit}>
|
||||
<Title level={4}>Reset Your Password</Title>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Form.Item
|
||||
name="password"
|
||||
validateTrigger="onBlur"
|
||||
rules={[{ required: true, message: 'Please enter password!' }]}
|
||||
>
|
||||
<Input.Password
|
||||
tabIndex={0}
|
||||
onChange={handleValuesChange}
|
||||
id="password"
|
||||
data-testid="password"
|
||||
/>
|
||||
</Form.Item>
|
||||
<AuthPageContainer>
|
||||
<div className="reset-password-card">
|
||||
<div className="reset-password-header">
|
||||
<div className="reset-password-header-icon">
|
||||
<KeyRound size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
// validateTrigger="onChange"
|
||||
validateTrigger="onBlur"
|
||||
rules={[{ required: true, message: 'Please enter confirm password!' }]}
|
||||
>
|
||||
<Input.Password
|
||||
onChange={handleValuesChange}
|
||||
id="confirmPassword"
|
||||
data-testid="confirmPassword"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Typography.Title level={4} className="reset-password-header-title">
|
||||
Reset Your Password
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="reset-password-header-subtitle">
|
||||
Monitor your applications. Find what is causing issues.
|
||||
</Typography.Paragraph>
|
||||
{version && (
|
||||
<div className="reset-password-version-badge">SigNoz {version}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{confirmPasswordError && (
|
||||
<Typography.Paragraph
|
||||
italic
|
||||
style={{
|
||||
color: '#D89614',
|
||||
marginTop: '0.50rem',
|
||||
}}
|
||||
>
|
||||
The passwords entered do not match. Please double-check and re-enter
|
||||
your passwords.
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<FormContainer
|
||||
form={form}
|
||||
onFinish={handleSubmit}
|
||||
className="reset-password-form"
|
||||
>
|
||||
<div className="reset-password-form-container">
|
||||
<div className="reset-password-form-fields">
|
||||
<div className="reset-password-field-container">
|
||||
<Label htmlFor="password">New Password</Label>
|
||||
<Form.Item
|
||||
name="password"
|
||||
validateTrigger="onBlur"
|
||||
rules={[{ required: true, message: 'Please enter password!' }]}
|
||||
>
|
||||
<AntdInput.Password
|
||||
tabIndex={0}
|
||||
onChange={handleValuesChange}
|
||||
onBlur={handlePasswordBlur}
|
||||
id="password"
|
||||
data-testid="password"
|
||||
placeholder="Enter new password"
|
||||
className="reset-password-form-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="reset-password-field-container">
|
||||
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
validateTrigger="onBlur"
|
||||
rules={[{ required: true, message: 'Please enter confirm password!' }]}
|
||||
>
|
||||
<AntdInput.Password
|
||||
onChange={handleValuesChange}
|
||||
onBlur={handleConfirmPasswordBlur}
|
||||
id="confirmPassword"
|
||||
data-testid="confirmPassword"
|
||||
placeholder="Confirm your new password"
|
||||
className="reset-password-form-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonContainer>
|
||||
{confirmPasswordError && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className="reset-password-error-callout"
|
||||
description="Passwords don't match. Please try again."
|
||||
/>
|
||||
)}
|
||||
|
||||
{errorMessage && !confirmPasswordError && (
|
||||
<AuthError error={errorMessage} />
|
||||
)}
|
||||
|
||||
<div className="reset-password-form-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
data-attr="signup"
|
||||
loading={loading}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-attr="reset-password"
|
||||
disabled={!isValidPassword || loading}
|
||||
className="reset-password-submit-button"
|
||||
suffixIcon={<ArrowRight size={16} />}
|
||||
>
|
||||
Get Started
|
||||
Reset Password
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</FormWrapper>
|
||||
</WelcomeLeftContainer>
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
import { Card, Form } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FormWrapper = styled(Card)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 432px;
|
||||
flex: 1;
|
||||
|
||||
.ant-card-body {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
margin-top: 1.8125rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const FormContainer = styled(Form)`
|
||||
& .ant-form-item {
|
||||
margin-bottom: 0px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getSubstituteVars } from 'api/dashboard/substitute_vars';
|
||||
import { prepareQueryRangePayloadV5 } from 'api/v5/v5';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -9,6 +10,7 @@ import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
@@ -71,11 +73,21 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
queryRangeMutation.mutate(queryPayload, {
|
||||
onSuccess: (data) => {
|
||||
const updatedQuery = mapQueryDataFromApi(data.data.compositeQuery);
|
||||
const url = `${ROUTES.ALERTS_NEW}?${
|
||||
QueryParams.compositeQuery
|
||||
}=${encodeURIComponent(JSON.stringify(updatedQuery))}&${
|
||||
QueryParams.panelTypes
|
||||
}=${widget.panelTypes}&version=${ENTITY_VERSION_V5}`;
|
||||
// If widget has a y-axis unit, set it to the updated query if it is not already set
|
||||
if (widget.yAxisUnit && !isEmpty(widget.yAxisUnit)) {
|
||||
updatedQuery.unit = widget.yAxisUnit;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
params.set(QueryParams.panelTypes, widget.panelTypes);
|
||||
params.set(QueryParams.version, ENTITY_VERSION_V5);
|
||||
params.set(QueryParams.source, YAxisSource.DASHBOARDS);
|
||||
|
||||
const url = `${ROUTES.ALERTS_NEW}?${params.toString()}`;
|
||||
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
},
|
||||
|
||||
@@ -1,116 +1,14 @@
|
||||
.login-page-container {
|
||||
height: 100vh;
|
||||
gap: 32px;
|
||||
.auth-form-card {
|
||||
width: 576px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.brand-container {
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
padding: 16px 0px;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
|
||||
color: var(--text-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.perilin-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: radial-gradient(circle, #fff 10%, transparent 0);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
|
||||
mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.login-page-content {
|
||||
width: 480px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
|
||||
background: rgb(18 19 23);
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.login-page-container {
|
||||
.brand-container {
|
||||
.brand-title {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.perilin-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: radial-gradient(circle, #000000 10%, transparent 0);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
|
||||
mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.login-page-content {
|
||||
background: rgb(255 255 255);
|
||||
border: 1px solid var(--border-vanilla-200);
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import './Login.styles.scss';
|
||||
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import LoginContainer from 'container/Login';
|
||||
|
||||
function Login(): JSX.Element {
|
||||
return (
|
||||
<div className="login-page-container">
|
||||
<div className="perilin-bg" />
|
||||
<div className="login-page-content">
|
||||
<div className="brand-container">
|
||||
<img
|
||||
src="/Logos/signoz-brand-logo.svg"
|
||||
alt="logo"
|
||||
className="brand-logo"
|
||||
/>
|
||||
|
||||
<div className="brand-title">SigNoz</div>
|
||||
</div>
|
||||
|
||||
<AuthPageContainer>
|
||||
<div className="auth-form-card">
|
||||
<LoginContainer />
|
||||
</div>
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import OnboardingQuestionaire from 'container/OnboardingQuestionaire';
|
||||
|
||||
function OrgOnboarding(): JSX.Element {
|
||||
return (
|
||||
<div className="onboarding-v2">
|
||||
<AuthPageContainer isOnboarding>
|
||||
<OnboardingQuestionaire />
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,213 +1,247 @@
|
||||
.signup-page-container {
|
||||
height: 100vh;
|
||||
gap: 32px;
|
||||
z-index: 1;
|
||||
|
||||
.signup-card {
|
||||
width: 576px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.brand-container {
|
||||
width: 100%;
|
||||
padding: 16px 0px;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
|
||||
color: var(--text-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.perilin-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: radial-gradient(circle, #fff 10%, transparent 0);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
|
||||
mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.signup-page-content {
|
||||
width: 540px;
|
||||
|
||||
.signup-form-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
.signup-header-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
background: rgb(18 19 23);
|
||||
.signup-header-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
z-index: 1;
|
||||
.signup-header-subtitle {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--semantic-secondary-foreground, #adb4c2);
|
||||
max-width: 360px;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.signup-form {
|
||||
.signup-form {
|
||||
width: 100%;
|
||||
|
||||
.signup-form-container {
|
||||
width: 100%;
|
||||
background: var(--semantic-secondary-background, #121317);
|
||||
border: 1px solid var(--semantic-secondary-border, #23262e);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
|
||||
.ant-input {
|
||||
height: 40px;
|
||||
.signup-form-fields {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper {
|
||||
height: 40px;
|
||||
.signup-field-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
height: auto;
|
||||
.signup-form-input,
|
||||
.signup-antd-input {
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
|
||||
&.ant-input,
|
||||
&.ant-input-password,
|
||||
&.ant-input-affix-wrapper {
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
.ant-input {
|
||||
height: auto;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup-form-input {
|
||||
height: 32px;
|
||||
background: var(--levels-l3-background, #23262e);
|
||||
border: 1px solid var(--levels-l3-border, #2c303a);
|
||||
border-radius: 2px;
|
||||
padding: 6px 8px;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--levels-l3-foreground, #747b8b);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--levels-l3-border, #2c303a);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup-form-header {
|
||||
.signup-form-header-text {
|
||||
color: var(--text-vanilla-300);
|
||||
}
|
||||
.signup-error-callout,
|
||||
.signup-info-callout {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.email-container,
|
||||
.first-name-container,
|
||||
.org-name-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.signup-error-callout {
|
||||
background: rgba(229, 72, 77, 0.1);
|
||||
border: 1px solid rgba(229, 72, 77, 0.2);
|
||||
border-radius: 4px;
|
||||
animation: horizontal-shaking 300ms ease-out;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
.signup-info-message {
|
||||
color: var(--semantic-secondary-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
margin: 24px 0 0 0;
|
||||
}
|
||||
|
||||
.signup-form-actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.signup-submit-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.password-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
|
||||
margin-top: 16px;
|
||||
|
||||
.password-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.password-error-container {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.password-error-message {
|
||||
color: var(--text-amber-400);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0px;
|
||||
text-align: left;
|
||||
text-underline-position: from-font;
|
||||
text-decoration-skip-ink: none;
|
||||
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.signup-info-message {
|
||||
color: var(--text-vanilla-300);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
|
||||
.signup-button-container {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.signup-form-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.signup-page-container {
|
||||
.brand-container {
|
||||
.brand-title {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.signup-card {
|
||||
.signup-form-header {
|
||||
.signup-form-header-text {
|
||||
.signup-header-icon {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
.signup-header-title {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
.signup-header-subtitle {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
}
|
||||
|
||||
.perilin-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.signup-form {
|
||||
.signup-form-container {
|
||||
background: var(--bg-base-white, #ffffff);
|
||||
border: 1px solid var(--bg-vanilla-300, #e9e9e9);
|
||||
|
||||
background: radial-gradient(circle, #000000 10%, transparent 0);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
.signup-form-input,
|
||||
.signup-antd-input {
|
||||
&.ant-input,
|
||||
&.ant-input-password,
|
||||
&.ant-input-affix-wrapper {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
-webkit-mask-image: radial-gradient(
|
||||
circle at 50% 0,
|
||||
rgba(11, 12, 14, 0.1) 0,
|
||||
rgba(11, 12, 14, 0) 100%
|
||||
);
|
||||
}
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
.signup-page-content {
|
||||
background: rgb(255 255 255);
|
||||
border: 1px solid var(--border-vanilla-200);
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
|
||||
color: var(--text-ink-500);
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
|
||||
.password-error-container {
|
||||
.password-error-message {
|
||||
color: var(--text-amber-400);
|
||||
.signup-form-input {
|
||||
background: var(--bg-vanilla-200, #f5f5f5);
|
||||
border-color: var(--bg-vanilla-300, #e9e9e9);
|
||||
color: var(--text-ink-500);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--semantic-primary-background, #4e74f8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signup-info-message {
|
||||
color: var(--text-ink-500);
|
||||
color: var(--text-neutral-light-200, #80828d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes horizontal-shaking {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import './SignUp.styles.scss';
|
||||
|
||||
import { Button, Form, Input, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Input as AntdInput, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import accept from 'api/v1/invite/id/accept';
|
||||
import getInviteDetails from 'api/v1/invite/id/get';
|
||||
import signUpApi from 'api/v1/register/post';
|
||||
import passwordAuthNContext from 'api/v2/sessions/email_password/post';
|
||||
import afterLogin from 'AppRoutes/utils';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArrowRight, CircleAlert } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
@@ -33,6 +38,7 @@ function SignUp(): JSX.Element {
|
||||
const [confirmPasswordError, setConfirmPasswordError] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [formError, setFormError] = useState<APIError | null>();
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const token = params.get('token');
|
||||
@@ -53,13 +59,17 @@ function SignUp(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
|
||||
// Watch form values for reactive validation
|
||||
const email = Form.useWatch('email', form);
|
||||
const password = Form.useWatch('password', form);
|
||||
const confirmPassword = Form.useWatch('confirmPassword', form);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getInviteDetailsResponse.status === 'success' &&
|
||||
getInviteDetailsResponse.data.data
|
||||
) {
|
||||
const responseDetails = getInviteDetailsResponse.data.data;
|
||||
form.setFieldValue('firstName', responseDetails.name);
|
||||
form.setFieldValue('email', responseDetails.email);
|
||||
form.setFieldValue('organizationName', responseDetails.organization);
|
||||
setIsDetailsDisable(true);
|
||||
@@ -97,7 +107,6 @@ function SignUp(): JSX.Element {
|
||||
]);
|
||||
|
||||
const isSignUp = token === null;
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const signUp = async (values: FormValues): Promise<void> => {
|
||||
try {
|
||||
@@ -117,7 +126,7 @@ function SignUp(): JSX.Element {
|
||||
|
||||
await afterLogin(token.data.accessToken, token.data.refreshToken);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setFormError(error as APIError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,10 +145,7 @@ function SignUp(): JSX.Element {
|
||||
|
||||
await afterLogin(token.data.accessToken, token.data.refreshToken);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: (error as APIError).getErrorCode(),
|
||||
description: (error as APIError).getErrorMessage(),
|
||||
});
|
||||
setFormError(error as APIError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,6 +155,7 @@ function SignUp(): JSX.Element {
|
||||
try {
|
||||
const values = form.getFieldsValue();
|
||||
setLoading(true);
|
||||
setFormError(null);
|
||||
|
||||
if (isSignUp) {
|
||||
await signUp(values);
|
||||
@@ -172,37 +179,57 @@ function SignUp(): JSX.Element {
|
||||
const handleValuesChange: (changedValues: Partial<FormValues>) => void = (
|
||||
changedValues,
|
||||
) => {
|
||||
// Clear error if passwords match while typing (but don't set error until blur)
|
||||
if ('password' in changedValues || 'confirmPassword' in changedValues) {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
|
||||
if (password && confirmPassword && password === confirmPassword) {
|
||||
setConfirmPasswordError(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordBlur = (): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
// Only validate if confirm password has a value
|
||||
if (confirmPassword) {
|
||||
const isSamePassword = password === confirmPassword;
|
||||
setConfirmPasswordError(!isSamePassword);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidForm: () => boolean = () => {
|
||||
const values = form.getFieldsValue();
|
||||
return (
|
||||
loading ||
|
||||
!values.email ||
|
||||
!values.password ||
|
||||
!values.confirmPassword ||
|
||||
confirmPasswordError
|
||||
);
|
||||
const handleConfirmPasswordBlur = (): void => {
|
||||
const { password, confirmPassword } = form.getFieldsValue();
|
||||
if (password && confirmPassword) {
|
||||
const isSamePassword = password === confirmPassword;
|
||||
setConfirmPasswordError(!isSamePassword);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="signup-page-container">
|
||||
<div className="perilin-bg" />
|
||||
<div className="signup-page-content">
|
||||
<div className="brand-container">
|
||||
<img
|
||||
src="/Logos/signoz-brand-logo.svg"
|
||||
alt="logo"
|
||||
className="brand-logo"
|
||||
/>
|
||||
const isValidForm = useMemo(
|
||||
(): boolean =>
|
||||
!loading &&
|
||||
Boolean(email?.trim()) &&
|
||||
Boolean(password?.trim()) &&
|
||||
Boolean(confirmPassword?.trim()) &&
|
||||
!confirmPasswordError,
|
||||
[loading, email, password, confirmPassword, confirmPasswordError],
|
||||
);
|
||||
|
||||
<div className="brand-title">SigNoz</div>
|
||||
return (
|
||||
<AuthPageContainer>
|
||||
<div className="signup-card">
|
||||
<div className="signup-form-header">
|
||||
<div className="signup-header-icon">
|
||||
<img src="/svgs/tv.svg" alt="TV" width="32" height="32" />
|
||||
</div>
|
||||
<Typography.Title level={4} className="signup-header-title">
|
||||
Create your account
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="signup-header-subtitle">
|
||||
You're almost in. Create a password to start monitoring your
|
||||
applications with SigNoz.
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<FormContainer
|
||||
@@ -211,75 +238,100 @@ function SignUp(): JSX.Element {
|
||||
form={form}
|
||||
className="signup-form"
|
||||
>
|
||||
<div className="signup-form-header">
|
||||
<Typography.Paragraph className="signup-form-header-text">
|
||||
You're almost in. Create a password to start monitoring your
|
||||
applications with SigNoz.
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<div className="signup-form-container">
|
||||
<div className="signup-form-fields">
|
||||
<div className="signup-field-container">
|
||||
<Label htmlFor="signupEmail">Email address</Label>
|
||||
<FormContainer.Item noStyle name="email">
|
||||
<Input
|
||||
placeholder="e.g. john@signoz.io"
|
||||
type="email"
|
||||
autoFocus
|
||||
required
|
||||
id="signupEmail"
|
||||
disabled={isDetailsDisable}
|
||||
className="signup-form-input"
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
|
||||
<div className="email-container">
|
||||
<Label htmlFor="signupEmail">Email</Label>
|
||||
<FormContainer.Item noStyle name="email">
|
||||
<Input
|
||||
placeholder="name@yourcompany.com"
|
||||
type="email"
|
||||
autoFocus
|
||||
required
|
||||
id="signupEmail"
|
||||
disabled={isDetailsDisable}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
<div className="signup-field-container">
|
||||
<Label htmlFor="currentPassword">Set your password</Label>
|
||||
<FormContainer.Item
|
||||
name="password"
|
||||
validateTrigger="onBlur"
|
||||
rules={[{ required: true, message: 'Please enter password!' }]}
|
||||
>
|
||||
<AntdInput.Password
|
||||
required
|
||||
id="currentPassword"
|
||||
placeholder="Enter new password"
|
||||
disabled={loading}
|
||||
className="signup-antd-input"
|
||||
onBlur={handlePasswordBlur}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
|
||||
<div className="password-container">
|
||||
<Label htmlFor="currentPassword">Password</Label>
|
||||
<FormContainer.Item noStyle name="password">
|
||||
<Input.Password required id="currentPassword" />
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
|
||||
<div className="password-container">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<FormContainer.Item noStyle name="confirmPassword">
|
||||
<Input.Password required id="confirmPassword" />
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
|
||||
<div className="password-error-container">
|
||||
{confirmPasswordError && (
|
||||
<Typography.Paragraph
|
||||
id="password-confirm-error"
|
||||
className="password-error-message"
|
||||
>
|
||||
Passwords don’t match. Please try again
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<div className="signup-field-container">
|
||||
<Label htmlFor="confirmPassword">Confirm your new password</Label>
|
||||
<FormContainer.Item
|
||||
name="confirmPassword"
|
||||
validateTrigger="onBlur"
|
||||
rules={[{ required: true, message: 'Please enter confirm password!' }]}
|
||||
>
|
||||
<AntdInput.Password
|
||||
required
|
||||
id="confirmPassword"
|
||||
placeholder="Confirm your new password"
|
||||
disabled={loading}
|
||||
className="signup-antd-input"
|
||||
onBlur={handleConfirmPasswordBlur}
|
||||
/>
|
||||
</FormContainer.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSignUp && (
|
||||
<Typography.Paragraph className="signup-info-message">
|
||||
* This will create an admin account. If you are not an admin, please ask
|
||||
your admin for an invite link
|
||||
</Typography.Paragraph>
|
||||
<Callout
|
||||
type="info"
|
||||
size="small"
|
||||
showIcon
|
||||
className="signup-info-callout"
|
||||
description="This will create an admin account. If you are not an admin, please ask your admin for an invite link"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="signup-button-container">
|
||||
{confirmPasswordError && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className="signup-error-callout"
|
||||
description="Passwords don't match. Please try again."
|
||||
/>
|
||||
)}
|
||||
|
||||
{formError && !confirmPasswordError && <AuthError error={formError} />}
|
||||
|
||||
<div className="signup-form-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-attr="signup"
|
||||
loading={loading}
|
||||
disabled={isValidForm()}
|
||||
className="periscope-btn primary next-btn"
|
||||
block
|
||||
disabled={!isValidForm}
|
||||
className="signup-submit-button"
|
||||
suffixIcon={<ArrowRight size={16} />}
|
||||
>
|
||||
Access My Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</div>
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
531
frontend/src/pages/SignUp/__tests__/SignUp.test.tsx
Normal file
531
frontend/src/pages/SignUp/__tests__/SignUp.test.tsx
Normal file
@@ -0,0 +1,531 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import afterLogin from 'AppRoutes/utils';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { InviteDetails } from 'types/api/user/getInviteDetails';
|
||||
import { SignupResponse } from 'types/api/v1/register/post';
|
||||
import { Token } from 'types/api/v2/sessions/email_password/post';
|
||||
|
||||
import SignUp from '../SignUp';
|
||||
|
||||
// Mock dependencies - must be before imports
|
||||
jest.mock('AppRoutes/utils', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockAfterLogin = jest.mocked(afterLogin);
|
||||
|
||||
jest.mock('api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const REGISTER_ENDPOINT = '*/api/v1/register';
|
||||
const EMAIL_PASSWORD_ENDPOINT = '*/api/v2/sessions/email_password';
|
||||
const INVITE_DETAILS_ENDPOINT = '*/api/v1/invite/*';
|
||||
const ACCEPT_INVITE_ENDPOINT = '*/api/v1/invite/accept';
|
||||
|
||||
interface MockSignupResponse extends SignupResponse {
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
const mockSignupResponse: MockSignupResponse = {
|
||||
orgId: 'test-org-id',
|
||||
createdAt: Date.now(),
|
||||
email: 'test@signoz.io',
|
||||
id: 'test-user-id',
|
||||
displayName: 'Test User',
|
||||
role: 'ADMIN',
|
||||
};
|
||||
|
||||
const mockTokenResponse: Token = {
|
||||
accessToken: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
};
|
||||
|
||||
const mockInviteDetails: InviteDetails = {
|
||||
email: 'invited@signoz.io',
|
||||
name: 'Invited User',
|
||||
organization: 'Test Org',
|
||||
createdAt: Date.now(),
|
||||
role: 'ADMIN',
|
||||
token: 'invite-token-123',
|
||||
};
|
||||
|
||||
describe('SignUp Component - Regular Signup', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAfterLogin.mockClear();
|
||||
window.history.pushState({}, '', '/signup');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Initial Render', () => {
|
||||
it('renders signup form with all required fields', () => {
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
expect(screen.getByText(/create your account/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/set your password/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText(/confirm your new password/i),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /access my workspace/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows info callout for admin account creation', () => {
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
expect(
|
||||
screen.getByText(/this will create an admin account/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('disables submit button when form is invalid', async () => {
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /access my workspace/i,
|
||||
});
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables submit button for partially filled fields', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
const emailInput = screen.getByLabelText(/email address/i);
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /access my workspace/i,
|
||||
});
|
||||
|
||||
// Missing email
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// Missing password
|
||||
await user.clear(passwordInput);
|
||||
await user.clear(confirmPasswordInput);
|
||||
await user.type(emailInput, 'test@signoz.io');
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// Missing confirm password
|
||||
await user.clear(confirmPasswordInput);
|
||||
await user.type(passwordInput, 'password123');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows error when passwords do not match', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password456');
|
||||
await user.tab(); // Blur the confirm password field to trigger validation
|
||||
|
||||
expect(
|
||||
await screen.findByText(/passwords don't match/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears password mismatch error when passwords match', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password456');
|
||||
await user.tab(); // Blur the confirm password field to trigger validation
|
||||
|
||||
expect(
|
||||
await screen.findByText(/passwords don't match/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.clear(confirmPasswordInput);
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
await user.tab(); // Blur again to trigger validation
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/passwords don't match/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful Signup', () => {
|
||||
it('successfully creates account and logs in user', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(REGISTER_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockSignupResponse,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post(EMAIL_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockTokenResponse,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
const emailInput = screen.getByLabelText(/email address/i);
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /access my workspace/i,
|
||||
});
|
||||
|
||||
await user.type(emailInput, 'test@signoz.io');
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAfterLogin).toHaveBeenCalledWith(
|
||||
'mock-access-token',
|
||||
'mock-refresh-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('displays error message when signup API fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.post(REGISTER_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'EMAIL_EXISTS',
|
||||
message: 'Email already exists',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, { initialRoute: '/signup' });
|
||||
|
||||
const emailInput = screen.getByLabelText(/email address/i);
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /access my workspace/i,
|
||||
});
|
||||
|
||||
await user.type(emailInput, 'existing@signoz.io');
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
const errorCallouts = await screen.findAllByText(/email already exists/i);
|
||||
expect(errorCallouts.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SignUp Component - Accept Invite', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
window.history.pushState({}, '', '/signup?token=invite-token-123');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('Initial Render with Invite', () => {
|
||||
it('pre-fills form fields from invite details', async () => {
|
||||
server.use(
|
||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockInviteDetails,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, {
|
||||
initialRoute: '/signup?token=invite-token-123',
|
||||
});
|
||||
|
||||
const emailInput = await screen.findByLabelText(/email address/i);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(emailInput).toHaveValue('invited@signoz.io');
|
||||
});
|
||||
});
|
||||
|
||||
it('disables email field when invite details are loaded', async () => {
|
||||
server.use(
|
||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockInviteDetails,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, {
|
||||
initialRoute: '/signup?token=invite-token-123',
|
||||
});
|
||||
|
||||
const emailInput = await screen.findByLabelText(/email address/i);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(emailInput).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show admin account info callout for invite flow', async () => {
|
||||
server.use(
|
||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockInviteDetails,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, {
|
||||
initialRoute: '/signup?token=invite-token-123',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/this will create an admin account/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful Invite Acceptance', () => {
|
||||
it('successfully accepts invite and logs in user', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockInviteDetails,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post(ACCEPT_INVITE_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockSignupResponse,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post(EMAIL_PASSWORD_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockTokenResponse,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, {
|
||||
initialRoute: '/signup?token=invite-token-123',
|
||||
});
|
||||
|
||||
const emailInput = await screen.findByLabelText(/email address/i);
|
||||
await waitFor(() => {
|
||||
expect(emailInput).toHaveValue('invited@signoz.io');
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /access my workspace/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAfterLogin).toHaveBeenCalledWith(
|
||||
'mock-access-token',
|
||||
'mock-refresh-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling for Invite', () => {
|
||||
it('displays error when invite details fetch fails', async () => {
|
||||
server.use(
|
||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(404),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INVITE_NOT_FOUND',
|
||||
message: 'Invite not found',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, {
|
||||
initialRoute: '/signup?token=invalid-token',
|
||||
});
|
||||
|
||||
// Verify form is still accessible and fields are enabled
|
||||
const emailInput = await screen.findByLabelText(/email address/i);
|
||||
|
||||
expect(emailInput).toBeInTheDocument();
|
||||
expect(emailInput).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('displays error when accept invite API fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get(INVITE_DETAILS_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: mockInviteDetails,
|
||||
status: 'success',
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.post(ACCEPT_INVITE_ENDPOINT, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INVALID_TOKEN',
|
||||
message: 'Invalid or expired invite token',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<SignUp />, undefined, {
|
||||
initialRoute: '/signup?token=expired-token',
|
||||
});
|
||||
|
||||
const emailInput = await screen.findByLabelText(/email address/i);
|
||||
await waitFor(() => {
|
||||
expect(emailInput).toHaveValue('invited@signoz.io');
|
||||
});
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText(/enter new password/i);
|
||||
const confirmPasswordInput = screen.getByPlaceholderText(
|
||||
/confirm your new password/i,
|
||||
);
|
||||
const submitButton = screen.getByRole('button', {
|
||||
name: /access my workspace/i,
|
||||
});
|
||||
|
||||
await user.type(passwordInput, 'password123');
|
||||
await user.type(confirmPasswordInput, 'password123');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
await user.click(submitButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/invalid or expired invite token/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,11 +10,17 @@ export const FormWrapper = styled(Card)`
|
||||
`;
|
||||
|
||||
export const Label = styled.label`
|
||||
margin-bottom: 11px;
|
||||
margin-top: 19px;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
line-height: 24px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--levels-l1-foreground, #eceef2);
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
|
||||
@@ -169,6 +169,10 @@
|
||||
|
||||
border: none;
|
||||
height: 36px;
|
||||
.ant-select-selection-search-input {
|
||||
min-width: max-content !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
|
||||
@@ -26,18 +26,6 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Theme transition animations
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
// For components that shouldn't transition (like loading spinners, animations)
|
||||
.no-transition,
|
||||
.no-transition * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
// Respect user's reduced motion preference
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
@@ -178,7 +166,7 @@ body {
|
||||
height: 11px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: transform 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
@@ -292,7 +280,7 @@ body {
|
||||
height: 11px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: transform 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -344,6 +344,8 @@ const customRender = (
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/export -- re-exporting custom render alongside @testing-library/react
|
||||
export * from '@testing-library/react';
|
||||
export { default as userEvent } from '@testing-library/user-event';
|
||||
// eslint-disable-next-line import/export -- custom render wraps the original
|
||||
export { customRender as render };
|
||||
|
||||
@@ -3135,7 +3135,10 @@ func (aH *APIHandler) getProducerConsumerEval(w http.ResponseWriter, r *http.Req
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval", kafkaSpanEval)
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorBadData,
|
||||
Err: err,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -887,7 +887,7 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
|
||||
|
||||
keys := make([]string, 0, len(queryRangeParams.Variables))
|
||||
|
||||
querytemplate.AssignReservedVarsV3(queryRangeParams)
|
||||
querytemplate.AssignReservedVars(queryRangeParams.Variables, queryRangeParams.Start, queryRangeParams.End)
|
||||
|
||||
for k := range queryRangeParams.Variables {
|
||||
keys = append(keys, k)
|
||||
@@ -927,7 +927,7 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
|
||||
continue
|
||||
}
|
||||
|
||||
querytemplate.AssignReservedVarsV3(queryRangeParams)
|
||||
querytemplate.AssignReservedVars(queryRangeParams.Variables, queryRangeParams.Start, queryRangeParams.End)
|
||||
|
||||
keys := make([]string, 0, len(queryRangeParams.Variables))
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,17 @@
|
||||
package converter
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/errors"
|
||||
|
||||
// Unit represents a unit of measurement
|
||||
type Unit string
|
||||
|
||||
func (u Unit) Validate() error {
|
||||
if !IsValidUnit(u) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid unit: %s", u)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value represents a value with a unit of measurement
|
||||
type Value struct {
|
||||
F float64
|
||||
@@ -60,6 +69,27 @@ func FromUnit(u Unit) Converter {
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidUnit returns true if the given unit is valid
|
||||
func IsValidUnit(u Unit) bool {
|
||||
switch u {
|
||||
// Duration unit
|
||||
case "ns", "us", "µs", "ms", "s", "m", "h", "d", "min",
|
||||
// Data unit
|
||||
"bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "deckbytes", "mbytes", "decMbytes", "decmbytes", "gbytes", "decGbytes", "decgbytes", "tbytes", "decTbytes", "dectbytes", "pbytes", "decPbytes", "decpbytes", "By", "kBy", "MBy", "GBy", "TBy", "PBy",
|
||||
// Data rate unit
|
||||
"binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits", "By/s", "kBy/s", "MBy/s", "GBy/s", "TBy/s", "PBy/s", "bit/s", "kbit/s", "Mbit/s", "Gbit/s", "Tbit/s", "Pbit/s",
|
||||
// Percent unit
|
||||
"percent", "percentunit", "%",
|
||||
// Bool unit
|
||||
"bool", "bool_yes_no", "bool_true_false", "bool_1_0",
|
||||
// Throughput unit
|
||||
"cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm", "{count}/s", "{ops}/s", "{req}/s", "{read}/s", "{write}/s", "{iops}/s", "{count}/min", "{ops}/min", "{read}/min", "{write}/min":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func UnitToName(u string) string {
|
||||
switch u {
|
||||
case "ns":
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/converter"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -601,43 +602,170 @@ func (c *CompositeQuery) Sanitize() {
|
||||
|
||||
func (c *CompositeQuery) Validate() error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("composite query is required")
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"composite query is required",
|
||||
)
|
||||
}
|
||||
|
||||
if c.BuilderQueries == nil && c.ClickHouseQueries == nil && c.PromQueries == nil && len(c.Queries) == 0 {
|
||||
return fmt.Errorf("composite query must contain at least one query type")
|
||||
}
|
||||
|
||||
if c.QueryType == QueryTypeBuilder {
|
||||
for name, query := range c.BuilderQueries {
|
||||
if err := query.Validate(c.PanelType); err != nil {
|
||||
return fmt.Errorf("builder query %s is invalid: %w", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.QueryType == QueryTypeClickHouseSQL {
|
||||
for name, query := range c.ClickHouseQueries {
|
||||
if err := query.Validate(); err != nil {
|
||||
return fmt.Errorf("clickhouse query %s is invalid: %w", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.QueryType == QueryTypePromQL {
|
||||
for name, query := range c.PromQueries {
|
||||
if err := query.Validate(); err != nil {
|
||||
return fmt.Errorf("prom query %s is invalid: %w", name, err)
|
||||
}
|
||||
// Validate unit if supplied
|
||||
if c.Unit != "" {
|
||||
unit := converter.Unit(c.Unit)
|
||||
err := unit.Validate()
|
||||
if err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid unit: %s",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.PanelType.Validate(); err != nil {
|
||||
return fmt.Errorf("panel type is invalid: %w", err)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"panel type is invalid: %s",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if err := c.QueryType.Validate(); err != nil {
|
||||
return fmt.Errorf("query type is invalid: %w", err)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"query type is invalid: %s",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if len(c.Queries) == 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"at least one query is required",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate each query
|
||||
for i, envelope := range c.Queries {
|
||||
queryId := qbtypes.GetQueryIdentifier(envelope, i)
|
||||
|
||||
switch envelope.Type {
|
||||
case qbtypes.QueryTypeBuilder, qbtypes.QueryTypeSubQuery:
|
||||
switch spec := envelope.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
if err := spec.Validate(qbtypes.RequestTypeTimeSeries); err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid %s: %s",
|
||||
queryId,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
if err := spec.Validate(qbtypes.RequestTypeTimeSeries); err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid %s: %s",
|
||||
queryId,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
if err := spec.Validate(qbtypes.RequestTypeTimeSeries); err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid %s: %s",
|
||||
queryId,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
default:
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"unknown query spec type for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
case qbtypes.QueryTypePromQL:
|
||||
spec, ok := envelope.Spec.(qbtypes.PromQuery)
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
if spec.Query == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"query expression is required for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
if err := validatePromQLQuery(spec.Query); err != nil {
|
||||
return err
|
||||
}
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
spec, ok := envelope.Spec.(qbtypes.ClickHouseQuery)
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
if spec.Query == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"query expression is required for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
if err := validateClickHouseQuery(spec.Query); err != nil {
|
||||
return err
|
||||
}
|
||||
case qbtypes.QueryTypeFormula:
|
||||
spec, ok := envelope.Spec.(qbtypes.QueryBuilderFormula)
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
if err := spec.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
spec, ok := envelope.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
queryId,
|
||||
)
|
||||
}
|
||||
err := spec.ValidateTraceOperator(c.Queries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"unknown query type '%s' for %s",
|
||||
envelope.Type,
|
||||
queryId,
|
||||
).WithAdditional(
|
||||
"Valid query types are: builder_query, builder_sub_query, builder_formula, builder_join, promql, clickhouse_sql, trace_operator",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all queries are disabled
|
||||
if allDisabled := checkQueriesDisabled(c); allDisabled {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"all queries are disabled - at least one query must be enabled",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1203,7 +1331,7 @@ func (f *FilterSet) Scan(src interface{}) error {
|
||||
func (f *FilterSet) Value() (driver.Value, error) {
|
||||
filterSetJson, err := json.Marshal(f)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not serialize FilterSet to JSON")
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "could not serialize FilterSet to JSON")
|
||||
}
|
||||
return filterSetJson, nil
|
||||
}
|
||||
|
||||
137
pkg/query-service/model/v3/validation.go
Normal file
137
pkg/query-service/model/v3/validation.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package v3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
clickhouse "github.com/AfterShip/clickhouse-sql-parser/parser"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
querytemplate "github.com/SigNoz/signoz/pkg/query-service/utils/queryTemplate"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
)
|
||||
|
||||
type QueryParseError struct {
|
||||
StartPosition *int
|
||||
EndPosition *int
|
||||
ErrorMessage string
|
||||
Query string
|
||||
}
|
||||
|
||||
func (e *QueryParseError) Error() string {
|
||||
if e.StartPosition != nil && e.EndPosition != nil {
|
||||
return fmt.Sprintf("query parse error: %s at position %d:%d", e.ErrorMessage, *e.StartPosition, *e.EndPosition)
|
||||
}
|
||||
return fmt.Sprintf("query parse error: %s", e.ErrorMessage)
|
||||
}
|
||||
|
||||
// validatePromQLQuery validates a PromQL query syntax using the Prometheus parser
|
||||
func validatePromQLQuery(query string) error {
|
||||
_, err := parser.ParseExpr(query)
|
||||
if err != nil {
|
||||
if syntaxErrs, ok := err.(parser.ParseErrors); ok {
|
||||
syntaxErr := syntaxErrs[0]
|
||||
startPosition := int(syntaxErr.PositionRange.Start)
|
||||
endPosition := int(syntaxErr.PositionRange.End)
|
||||
return &QueryParseError{
|
||||
StartPosition: &startPosition,
|
||||
EndPosition: &endPosition,
|
||||
ErrorMessage: syntaxErr.Error(),
|
||||
Query: query,
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// validateClickHouseQuery validates a ClickHouse SQL query syntax using the ClickHouse parser
|
||||
func validateClickHouseQuery(query string) error {
|
||||
// Assign the default template variables with dummy values
|
||||
variables := make(map[string]interface{})
|
||||
start := time.Now().UnixMilli()
|
||||
end := start + 1000
|
||||
querytemplate.AssignReservedVars(variables, start, end)
|
||||
|
||||
// Apply the values for default template variables before parsing the query
|
||||
tmpl := template.New("clickhouse-query")
|
||||
tmpl, err := tmpl.Parse(query)
|
||||
if err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"failed to parse clickhouse query: %s",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
var queryBuffer bytes.Buffer
|
||||
err = tmpl.Execute(&queryBuffer, variables)
|
||||
if err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"failed to execute clickhouse query template: %s",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
// Parse the ClickHouse query with the default template variables applied
|
||||
p := clickhouse.NewParser(queryBuffer.String())
|
||||
_, err = p.ParseStmts()
|
||||
if err != nil {
|
||||
// TODO: errors returned here is errors.errorString
|
||||
// we should think on using some other library that parses the CH query in more accurate manner,
|
||||
// current CH parser does very minimal checks and on just the known keywords, without validating the syntax of given query.
|
||||
// Sample Error: "line 0:36 expected table name or subquery, got ;\nSELECT department, avg(salary) FROM ;\n ^\n"
|
||||
return &QueryParseError{
|
||||
ErrorMessage: err.Error(),
|
||||
Query: query,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkQueriesDisabled checks if all queries are disabled. Returns true if all queries are disabled, false otherwise.
|
||||
func checkQueriesDisabled(compositeQuery *CompositeQuery) bool {
|
||||
for _, envelope := range compositeQuery.Queries {
|
||||
switch envelope.Type {
|
||||
case qbtypes.QueryTypeBuilder, qbtypes.QueryTypeSubQuery:
|
||||
switch spec := envelope.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
if !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
if !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
if !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case qbtypes.QueryTypeFormula:
|
||||
if spec, ok := envelope.Spec.(qbtypes.QueryBuilderFormula); ok && !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
if spec, ok := envelope.Spec.(qbtypes.QueryBuilderTraceOperator); ok && !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
case qbtypes.QueryTypeJoin:
|
||||
if spec, ok := envelope.Spec.(qbtypes.QueryBuilderJoin); ok && !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
case qbtypes.QueryTypePromQL:
|
||||
if spec, ok := envelope.Spec.(qbtypes.PromQuery); ok && !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
if spec, ok := envelope.Spec.(qbtypes.ClickHouseQuery); ok && !spec.Disabled {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, all queries are disabled
|
||||
return true
|
||||
}
|
||||
528
pkg/query-service/model/v3/validation_test.go
Normal file
528
pkg/query-service/model/v3/validation_test.go
Normal file
@@ -0,0 +1,528 @@
|
||||
package v3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateCompositeQuery(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
compositeQuery *CompositeQuery
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "nil composite query should return error",
|
||||
compositeQuery: nil,
|
||||
wantErr: true,
|
||||
errContains: "composite query is required",
|
||||
},
|
||||
{
|
||||
name: "empty queries array should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "at least one query is required",
|
||||
},
|
||||
{
|
||||
name: "invalid input error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Unit: "some_invalid_unit",
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m])",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid unit",
|
||||
},
|
||||
{
|
||||
name: "valid metric builder query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Unit: "bytes", // valid unit
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "metric_query",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid log builder query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Unit: "µs", // valid unit
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Name: "log_query",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid trace builder query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Unit: "MBs", // valid unit
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "trace_query",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid PromQL query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Unit: "{req}/s", // valid unit
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m])",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid ClickHouse query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeClickHouseSQL,
|
||||
Spec: qbtypes.ClickHouseQuery{
|
||||
Name: "ch_query",
|
||||
Query: "SELECT count(*) FROM metrics WHERE metric_name = 'cpu_usage'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid formula query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "formula_query",
|
||||
Expression: "A + B",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
// We've not added support for join query yet
|
||||
{
|
||||
name: "valid join query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeJoin,
|
||||
Spec: qbtypes.QueryBuilderJoin{
|
||||
Name: "join_query",
|
||||
Left: qbtypes.QueryRef{Name: "A"},
|
||||
Right: qbtypes.QueryRef{Name: "B"},
|
||||
Type: qbtypes.JoinTypeInner,
|
||||
On: "service_name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unknown query type",
|
||||
},
|
||||
{
|
||||
name: "valid trace operator query should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Aggregations: []qbtypes.TraceAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeTraceOperator,
|
||||
Spec: qbtypes.QueryBuilderTraceOperator{
|
||||
Name: "trace_operator",
|
||||
Expression: "A && B",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid metric builder query - missing aggregation should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "metric_query",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid",
|
||||
},
|
||||
{
|
||||
name: "invalid PromQL query - empty query should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "query expression is required",
|
||||
},
|
||||
{
|
||||
name: "invalid PromQL query - syntax error should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unclosed left parenthesis",
|
||||
},
|
||||
{
|
||||
name: "invalid ClickHouse query - empty query should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeClickHouseSQL,
|
||||
Spec: qbtypes.ClickHouseQuery{
|
||||
Name: "ch_query",
|
||||
Query: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "query expression is required",
|
||||
},
|
||||
{
|
||||
name: "invalid ClickHouse query - syntax error should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeClickHouseSQL,
|
||||
Spec: qbtypes.ClickHouseQuery{
|
||||
Name: "ch_query",
|
||||
Query: "SELECT * FROM metrics WHERE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "query parse error",
|
||||
},
|
||||
{
|
||||
name: "invalid formula query - empty expression should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeFormula,
|
||||
Spec: qbtypes.QueryBuilderFormula{
|
||||
Name: "formula_query",
|
||||
Expression: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "formula expression cannot be blank",
|
||||
},
|
||||
{
|
||||
name: "invalid trace operator query - empty expression should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeTraceOperator,
|
||||
Spec: qbtypes.QueryBuilderTraceOperator{
|
||||
Name: "trace_operator",
|
||||
Expression: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "expression cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "all queries disabled should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "metric_query",
|
||||
Disabled: true,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m])",
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "all queries are disabled",
|
||||
},
|
||||
{
|
||||
name: "mixed disabled and enabled queries should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "metric_query",
|
||||
Disabled: true,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m])",
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple valid queries should pass",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "metric_query",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m])",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeClickHouseSQL,
|
||||
Spec: qbtypes.ClickHouseQuery{
|
||||
Name: "ch_query",
|
||||
Query: "SELECT count(*) FROM metrics WHERE metric_name = 'cpu_usage'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid query in multiple queries should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Name: "metric_query",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypePromQL,
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "invalid promql syntax [",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "query parse error",
|
||||
},
|
||||
{
|
||||
name: "unknown query type should return error",
|
||||
compositeQuery: &CompositeQuery{
|
||||
QueryType: QueryTypeBuilder,
|
||||
PanelType: PanelTypeGraph,
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryType{String: valuer.NewString("invalid_query_type")},
|
||||
Spec: qbtypes.PromQuery{
|
||||
Name: "prom_query",
|
||||
Query: "rate(http_requests_total[5m])",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unknown query type",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.compositeQuery.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
if tt.errContains != "" {
|
||||
require.Contains(t, err.Error(), tt.errContains)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (r *ThresholdRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v
|
||||
Variables: make(map[string]interface{}, 0),
|
||||
NoCache: true,
|
||||
}
|
||||
querytemplate.AssignReservedVarsV3(params)
|
||||
querytemplate.AssignReservedVars(params.Variables, start, end)
|
||||
for name, chQuery := range r.ruleCondition.CompositeQuery.ClickHouseQueries {
|
||||
if chQuery.Disabled {
|
||||
continue
|
||||
|
||||
@@ -2,26 +2,21 @@ package querytemplate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
)
|
||||
|
||||
// AssignReservedVars assigns values for go template vars. assumes that
|
||||
// model.QueryRangeParamsV3.Start and End are Unix Nano timestamps
|
||||
func AssignReservedVarsV3(queryRangeParams *v3.QueryRangeParamsV3) {
|
||||
queryRangeParams.Variables["start_timestamp"] = queryRangeParams.Start / 1000
|
||||
queryRangeParams.Variables["end_timestamp"] = queryRangeParams.End / 1000
|
||||
func AssignReservedVars(variables map[string]interface{}, start int64, end int64) {
|
||||
variables["start_timestamp"] = start / 1000
|
||||
variables["end_timestamp"] = end / 1000
|
||||
|
||||
queryRangeParams.Variables["start_timestamp_ms"] = queryRangeParams.Start
|
||||
queryRangeParams.Variables["end_timestamp_ms"] = queryRangeParams.End
|
||||
variables["start_timestamp_ms"] = start
|
||||
variables["end_timestamp_ms"] = end
|
||||
|
||||
queryRangeParams.Variables["SIGNOZ_START_TIME"] = queryRangeParams.Start
|
||||
queryRangeParams.Variables["SIGNOZ_END_TIME"] = queryRangeParams.End
|
||||
variables["SIGNOZ_START_TIME"] = start
|
||||
variables["SIGNOZ_END_TIME"] = end
|
||||
|
||||
queryRangeParams.Variables["start_timestamp_nano"] = queryRangeParams.Start * 1e6
|
||||
queryRangeParams.Variables["end_timestamp_nano"] = queryRangeParams.End * 1e6
|
||||
|
||||
queryRangeParams.Variables["start_datetime"] = fmt.Sprintf("toDateTime(%d)", queryRangeParams.Start/1000)
|
||||
queryRangeParams.Variables["end_datetime"] = fmt.Sprintf("toDateTime(%d)", queryRangeParams.End/1000)
|
||||
variables["start_timestamp_nano"] = start * 1e6
|
||||
variables["end_timestamp_nano"] = end * 1e6
|
||||
|
||||
variables["start_datetime"] = fmt.Sprintf("toDateTime(%d)", start/1000)
|
||||
variables["end_datetime"] = fmt.Sprintf("toDateTime(%d)", end/1000)
|
||||
}
|
||||
|
||||
@@ -553,6 +553,11 @@ func (f Function) Copy() Function {
|
||||
return c
|
||||
}
|
||||
|
||||
// Validate validates the Function by calling Validate on its Name
|
||||
func (f Function) Validate() error {
|
||||
return f.Name.Validate()
|
||||
}
|
||||
|
||||
type LimitBy struct {
|
||||
// keys to limit by
|
||||
Keys []string `json:"keys"`
|
||||
|
||||
@@ -73,6 +73,43 @@ func (f *QueryBuilderFormula) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the QueryBuilderFormula fields are valid
|
||||
func (f QueryBuilderFormula) Validate() error {
|
||||
// Validate name is not blank
|
||||
if strings.TrimSpace(f.Name) == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"formula name cannot be blank",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate expression is not blank
|
||||
if strings.TrimSpace(f.Expression) == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"formula expression cannot be blank",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate functions if present
|
||||
for i, fn := range f.Functions {
|
||||
if err := fn.Validate(); err != nil {
|
||||
fnId := fmt.Sprintf("function #%d", i+1)
|
||||
if f.Name != "" {
|
||||
fnId = fmt.Sprintf("function #%d in formula '%s'", i+1, f.Name)
|
||||
}
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid %s: %s",
|
||||
fnId,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// small container to store the query name and index or alias reference
|
||||
// for a variable in the formula expression
|
||||
// read below for more details on aggregation references
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -33,6 +34,37 @@ var (
|
||||
FunctionNameFillZero = FunctionName{valuer.NewString("fillZero")}
|
||||
)
|
||||
|
||||
// Validate checks if the FunctionName is valid and one of the known types
|
||||
func (fn FunctionName) Validate() error {
|
||||
switch fn {
|
||||
case FunctionNameCutOffMin,
|
||||
FunctionNameCutOffMax,
|
||||
FunctionNameClampMin,
|
||||
FunctionNameClampMax,
|
||||
FunctionNameAbsolute,
|
||||
FunctionNameRunningDiff,
|
||||
FunctionNameLog2,
|
||||
FunctionNameLog10,
|
||||
FunctionNameCumulativeSum,
|
||||
FunctionNameEWMA3,
|
||||
FunctionNameEWMA5,
|
||||
FunctionNameEWMA7,
|
||||
FunctionNameMedian3,
|
||||
FunctionNameMedian5,
|
||||
FunctionNameMedian7,
|
||||
FunctionNameTimeShift,
|
||||
FunctionNameAnomaly,
|
||||
FunctionNameFillZero:
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid function name: %s",
|
||||
fn.StringValue(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyFunction applies the given function to the result data
|
||||
func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
|
||||
// Extract the function name and arguments
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// getQueryIdentifier returns a friendly identifier for a query based on its type and name/content
|
||||
func getQueryIdentifier(envelope QueryEnvelope, index int) string {
|
||||
// GetQueryIdentifier returns a friendly identifier for a query based on its type and name/content
|
||||
func GetQueryIdentifier(envelope QueryEnvelope, index int) string {
|
||||
switch envelope.Type {
|
||||
case QueryTypeBuilder, QueryTypeSubQuery:
|
||||
switch spec := envelope.Spec.(type) {
|
||||
@@ -567,7 +567,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
switch spec := envelope.Spec.(type) {
|
||||
case QueryBuilderQuery[TraceAggregation]:
|
||||
if err := spec.Validate(r.RequestType); err != nil {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return wrapValidationError(err, queryId, "invalid %s: %s")
|
||||
}
|
||||
// Check name uniqueness for non-formula context
|
||||
@@ -583,7 +583,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
}
|
||||
case QueryBuilderQuery[LogAggregation]:
|
||||
if err := spec.Validate(r.RequestType); err != nil {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return wrapValidationError(err, queryId, "invalid %s: %s")
|
||||
}
|
||||
// Check name uniqueness for non-formula context
|
||||
@@ -599,7 +599,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
}
|
||||
case QueryBuilderQuery[MetricAggregation]:
|
||||
if err := spec.Validate(r.RequestType); err != nil {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return wrapValidationError(err, queryId, "invalid %s: %s")
|
||||
}
|
||||
// Check name uniqueness for non-formula context
|
||||
@@ -614,7 +614,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
queryNames[spec.Name] = true
|
||||
}
|
||||
default:
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"unknown spec type for %s",
|
||||
@@ -625,7 +625,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
// Formula validation is handled separately
|
||||
spec, ok := envelope.Spec.(QueryBuilderFormula)
|
||||
if !ok {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
@@ -633,7 +633,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
)
|
||||
}
|
||||
if spec.Expression == "" {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"expression is required for %s",
|
||||
@@ -644,7 +644,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
// Join validation is handled separately
|
||||
_, ok := envelope.Spec.(QueryBuilderJoin)
|
||||
if !ok {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
@@ -654,7 +654,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
case QueryTypeTraceOperator:
|
||||
spec, ok := envelope.Spec.(QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
@@ -662,7 +662,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
)
|
||||
}
|
||||
if spec.Expression == "" {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"expression is required for %s",
|
||||
@@ -673,7 +673,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
// PromQL validation is handled separately
|
||||
spec, ok := envelope.Spec.(PromQuery)
|
||||
if !ok {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
@@ -681,7 +681,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
)
|
||||
}
|
||||
if spec.Query == "" {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"query expression is required for %s",
|
||||
@@ -692,7 +692,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
// ClickHouse SQL validation is handled separately
|
||||
spec, ok := envelope.Spec.(ClickHouseQuery)
|
||||
if !ok {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid spec for %s",
|
||||
@@ -700,7 +700,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
)
|
||||
}
|
||||
if spec.Query == "" {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"query expression is required for %s",
|
||||
@@ -708,7 +708,7 @@ func (r *QueryRangeRequest) validateCompositeQuery() error {
|
||||
)
|
||||
}
|
||||
default:
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"unknown query type '%s' for %s",
|
||||
@@ -735,7 +735,7 @@ func (c *CompositeQuery) Validate(requestType RequestType) error {
|
||||
// Validate each query
|
||||
for i, envelope := range c.Queries {
|
||||
if err := validateQueryEnvelope(envelope, requestType); err != nil {
|
||||
queryId := getQueryIdentifier(envelope, i)
|
||||
queryId := GetQueryIdentifier(envelope, i)
|
||||
return wrapValidationError(err, queryId, "invalid %s: %s")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ func TestParseIntoRuleSchemaVersioning(t *testing.T) {
|
||||
"spec": [{
|
||||
"name": "existing_threshold",
|
||||
"target": 50.0,
|
||||
"targetUnit": "MB",
|
||||
"targetUnit": "MBs",
|
||||
"ruleUnit": "bytes",
|
||||
"matchType": "1",
|
||||
"op": "1"
|
||||
|
||||
@@ -252,6 +252,15 @@ func (b BasicRuleThreshold) Validate() error {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid match type: %s", string(b.MatchType)))
|
||||
}
|
||||
|
||||
// Only validate unit if specified
|
||||
if b.TargetUnit != "" {
|
||||
unit := converter.Unit(b.TargetUnit)
|
||||
err := unit.Validate()
|
||||
if err != nil {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid unit"))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,124 @@ class MetricsSample(ABC):
|
||||
]
|
||||
|
||||
|
||||
class MetricsExpHist(ABC):
|
||||
"""Represents a row in the exp_hist table for exponential histograms."""
|
||||
|
||||
env: str
|
||||
temporality: str
|
||||
metric_name: str
|
||||
fingerprint: np.uint64
|
||||
unix_milli: np.int64
|
||||
count: np.uint64
|
||||
sum: np.float64
|
||||
min: np.float64
|
||||
max: np.float64
|
||||
sketch: bytes
|
||||
flags: np.uint32
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
metric_name: str,
|
||||
fingerprint: np.uint64,
|
||||
timestamp: datetime.datetime,
|
||||
count: int,
|
||||
sum_value: float,
|
||||
min_value: float,
|
||||
max_value: float,
|
||||
sketch: bytes = b"",
|
||||
temporality: str = "Unspecified",
|
||||
env: str = "default",
|
||||
flags: int = 0,
|
||||
) -> None:
|
||||
self.env = env
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.fingerprint = fingerprint
|
||||
self.unix_milli = np.int64(int(timestamp.timestamp() * 1e3))
|
||||
self.count = np.uint64(count)
|
||||
self.sum = np.float64(sum_value)
|
||||
self.min = np.float64(min_value)
|
||||
self.max = np.float64(max_value)
|
||||
self.sketch = sketch
|
||||
self.flags = np.uint32(flags)
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.env,
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.fingerprint,
|
||||
self.unix_milli,
|
||||
self.count,
|
||||
self.sum,
|
||||
self.min,
|
||||
self.max,
|
||||
self.sketch,
|
||||
self.flags,
|
||||
]
|
||||
|
||||
|
||||
class MetricsMetadata(ABC):
|
||||
"""Represents a row in the metadata table for metric metadata."""
|
||||
|
||||
temporality: str
|
||||
metric_name: str
|
||||
description: str
|
||||
unit: str
|
||||
type: str
|
||||
is_monotonic: bool
|
||||
attr_name: str
|
||||
attr_type: str
|
||||
attr_datatype: str
|
||||
attr_string_value: str
|
||||
first_reported_unix_milli: np.int64
|
||||
last_reported_unix_milli: np.int64
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
metric_name: str,
|
||||
attr_name: str,
|
||||
attr_type: str,
|
||||
attr_datatype: str,
|
||||
attr_string_value: str,
|
||||
timestamp: datetime.datetime,
|
||||
temporality: str = "Unspecified",
|
||||
description: str = "",
|
||||
unit: str = "",
|
||||
type_: str = "Sum",
|
||||
is_monotonic: bool = True,
|
||||
) -> None:
|
||||
self.temporality = temporality
|
||||
self.metric_name = metric_name
|
||||
self.description = description
|
||||
self.unit = unit
|
||||
self.type = type_
|
||||
self.is_monotonic = is_monotonic
|
||||
self.attr_name = attr_name
|
||||
self.attr_type = attr_type
|
||||
self.attr_datatype = attr_datatype
|
||||
self.attr_string_value = attr_string_value
|
||||
unix_milli = np.int64(int(timestamp.timestamp() * 1e3))
|
||||
self.first_reported_unix_milli = unix_milli
|
||||
self.last_reported_unix_milli = unix_milli
|
||||
|
||||
def to_row(self) -> list:
|
||||
return [
|
||||
self.temporality,
|
||||
self.metric_name,
|
||||
self.description,
|
||||
self.unit,
|
||||
self.type,
|
||||
self.is_monotonic,
|
||||
self.attr_name,
|
||||
self.attr_type,
|
||||
self.attr_datatype,
|
||||
self.attr_string_value,
|
||||
self.first_reported_unix_milli,
|
||||
self.last_reported_unix_milli,
|
||||
]
|
||||
|
||||
|
||||
class Metrics(ABC):
|
||||
"""High-level metric representation. Produces both time series and sample entries."""
|
||||
|
||||
@@ -189,6 +307,119 @@ class Metrics(ABC):
|
||||
flags=flags,
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"metric_name": self.metric_name,
|
||||
"labels": self.labels,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"value": self.value,
|
||||
"temporality": self.temporality,
|
||||
"type_": self._time_series.type,
|
||||
"is_monotonic": self._time_series.is_monotonic,
|
||||
"flags": self.flags,
|
||||
"description": self._time_series.description,
|
||||
"unit": self._time_series.unit,
|
||||
"env": self._time_series.env,
|
||||
"resource_attrs": self._time_series.resource_attrs,
|
||||
"scope_attrs": self._time_series.scope_attrs,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(
|
||||
cls,
|
||||
data: dict,
|
||||
# base_time: Optional[datetime.datetime] = None,
|
||||
metric_name_override: Optional[str] = None,
|
||||
) -> "Metrics":
|
||||
"""
|
||||
Create a Metrics instance from a dict.
|
||||
|
||||
Args:
|
||||
data: The dict containing metric data
|
||||
base_time: If provided, timestamps are shifted relative to this time.
|
||||
The earliest timestamp in the data becomes base_time.
|
||||
metric_name_override: If provided, overrides the metric_name from data
|
||||
"""
|
||||
# parse timestamp from iso format
|
||||
ts_str = data["timestamp"]
|
||||
if ts_str.endswith("Z"):
|
||||
ts_str = ts_str[:-1] + "+00:00"
|
||||
timestamp = datetime.datetime.fromisoformat(ts_str)
|
||||
|
||||
return cls(
|
||||
metric_name=metric_name_override or data["metric_name"],
|
||||
labels=data.get("labels", {}),
|
||||
timestamp=timestamp,
|
||||
value=data["value"],
|
||||
temporality=data.get("temporality", "Unspecified"),
|
||||
flags=data.get("flags", 0),
|
||||
description=data.get("description", ""),
|
||||
unit=data.get("unit", ""),
|
||||
type_=data.get("type_", "Sum"),
|
||||
is_monotonic=data.get("is_monotonic", True),
|
||||
env=data.get("env", "default"),
|
||||
resource_attributes=data.get("resource_attrs", {}),
|
||||
scope_attributes=data.get("scope_attrs", {}),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_from_file(
|
||||
cls,
|
||||
file_path: str,
|
||||
base_time: Optional[datetime.datetime] = None,
|
||||
metric_name_override: Optional[str] = None,
|
||||
) -> List["Metrics"]:
|
||||
"""
|
||||
Load metrics from a JSONL file.
|
||||
|
||||
Each line should be a JSON object representing a metric.
|
||||
|
||||
Args:
|
||||
file_path: Path to the JSONL file
|
||||
base_time: If provided, all timestamps are shifted so the earliest
|
||||
timestamp in the file maps to base_time
|
||||
metric_name_override: If provided, overrides metric_name for all metrics
|
||||
"""
|
||||
data_list = []
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
data_list.append(json.loads(line))
|
||||
|
||||
if not data_list:
|
||||
return []
|
||||
|
||||
# If base_time provided, calculate time offset
|
||||
time_offset = datetime.timedelta(0)
|
||||
if base_time is not None:
|
||||
# Find earliest timestamp
|
||||
earliest = None
|
||||
for data in data_list:
|
||||
ts_str = data["timestamp"]
|
||||
if ts_str.endswith("Z"):
|
||||
ts_str = ts_str[:-1] + "+00:00"
|
||||
ts = datetime.datetime.fromisoformat(ts_str)
|
||||
if earliest is None or ts < earliest:
|
||||
earliest = ts
|
||||
if earliest is not None:
|
||||
time_offset = base_time - earliest
|
||||
|
||||
metrics = []
|
||||
for data in data_list:
|
||||
ts_str = data["timestamp"]
|
||||
if ts_str.endswith("Z"):
|
||||
ts_str = ts_str[:-1] + "+00:00"
|
||||
original_ts = datetime.datetime.fromisoformat(ts_str)
|
||||
adjusted_ts = original_ts + time_offset
|
||||
data["timestamp"] = adjusted_ts.isoformat()
|
||||
metrics.append(
|
||||
cls.from_dict(data, metric_name_override=metric_name_override)
|
||||
)
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
@pytest.fixture(name="insert_metrics", scope="function")
|
||||
def insert_metrics(
|
||||
@@ -200,6 +431,7 @@ def insert_metrics(
|
||||
This function handles insertion into:
|
||||
- distributed_time_series_v4 (time series metadata)
|
||||
- distributed_samples_v4 (actual sample values)
|
||||
- distributed_metadata (metric attribute metadata)
|
||||
"""
|
||||
time_series_map: dict[int, MetricsTimeSeries] = {}
|
||||
for metric in metrics:
|
||||
@@ -247,15 +479,93 @@ def insert_metrics(
|
||||
data=[sample.to_row() for sample in samples],
|
||||
)
|
||||
|
||||
# (metric_name, attr_type, attr_name, attr_value) -> MetricsMetadata
|
||||
metadata_map: dict[tuple, MetricsMetadata] = {}
|
||||
for metric in metrics:
|
||||
ts = metric.time_series
|
||||
for attr_name, attr_value in metric.labels.items():
|
||||
key = (ts.metric_name, "point", attr_name, str(attr_value))
|
||||
if key not in metadata_map:
|
||||
metadata_map[key] = MetricsMetadata(
|
||||
metric_name=ts.metric_name,
|
||||
attr_name=attr_name,
|
||||
attr_type="point",
|
||||
attr_datatype="String",
|
||||
attr_string_value=str(attr_value),
|
||||
timestamp=metric.timestamp,
|
||||
temporality=ts.temporality,
|
||||
description=ts.description,
|
||||
unit=ts.unit,
|
||||
type_=ts.type,
|
||||
is_monotonic=ts.is_monotonic,
|
||||
)
|
||||
for attr_name, attr_value in ts.resource_attrs.items():
|
||||
key = (ts.metric_name, "resource", attr_name, str(attr_value))
|
||||
if key not in metadata_map:
|
||||
metadata_map[key] = MetricsMetadata(
|
||||
metric_name=ts.metric_name,
|
||||
attr_name=attr_name,
|
||||
attr_type="resource",
|
||||
attr_datatype="String",
|
||||
attr_string_value=str(attr_value),
|
||||
timestamp=metric.timestamp,
|
||||
temporality=ts.temporality,
|
||||
description=ts.description,
|
||||
unit=ts.unit,
|
||||
type_=ts.type,
|
||||
is_monotonic=ts.is_monotonic,
|
||||
)
|
||||
for attr_name, attr_value in ts.scope_attrs.items():
|
||||
key = (ts.metric_name, "scope", attr_name, str(attr_value))
|
||||
if key not in metadata_map:
|
||||
metadata_map[key] = MetricsMetadata(
|
||||
metric_name=ts.metric_name,
|
||||
attr_name=attr_name,
|
||||
attr_type="scope",
|
||||
attr_datatype="String",
|
||||
attr_string_value=str(attr_value),
|
||||
timestamp=metric.timestamp,
|
||||
temporality=ts.temporality,
|
||||
description=ts.description,
|
||||
unit=ts.unit,
|
||||
type_=ts.type,
|
||||
is_monotonic=ts.is_monotonic,
|
||||
)
|
||||
|
||||
if len(metadata_map) > 0:
|
||||
clickhouse.conn.insert(
|
||||
database="signoz_metrics",
|
||||
table="distributed_metadata",
|
||||
column_names=[
|
||||
"temporality",
|
||||
"metric_name",
|
||||
"description",
|
||||
"unit",
|
||||
"type",
|
||||
"is_monotonic",
|
||||
"attr_name",
|
||||
"attr_type",
|
||||
"attr_datatype",
|
||||
"attr_string_value",
|
||||
"first_reported_unix_milli",
|
||||
"last_reported_unix_milli",
|
||||
],
|
||||
data=[m.to_row() for m in metadata_map.values()],
|
||||
)
|
||||
|
||||
yield _insert_metrics
|
||||
|
||||
# Cleanup
|
||||
clickhouse.conn.query(
|
||||
f"TRUNCATE TABLE signoz_metrics.time_series_v4 ON CLUSTER '{clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' SYNC"
|
||||
)
|
||||
clickhouse.conn.query(
|
||||
f"TRUNCATE TABLE signoz_metrics.samples_v4 ON CLUSTER '{clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' SYNC"
|
||||
)
|
||||
cluster = clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
|
||||
tables_to_truncate = [
|
||||
"time_series_v4",
|
||||
"samples_v4",
|
||||
"exp_hist",
|
||||
"metadata",
|
||||
]
|
||||
for table in tables_to_truncate:
|
||||
clickhouse.conn.query(
|
||||
f"TRUNCATE TABLE signoz_metrics.{table} ON CLUSTER '{cluster}' SYNC"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="remove_metrics_ttl_and_storage_settings", scope="function")
|
||||
@@ -272,15 +582,18 @@ def remove_metrics_ttl_and_storage_settings(signoz: types.SigNoz):
|
||||
"time_series_v4_6hrs",
|
||||
"time_series_v4_1day",
|
||||
"time_series_v4_1week",
|
||||
"exp_hist",
|
||||
"metadata",
|
||||
]
|
||||
|
||||
cluster = signoz.telemetrystore.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
|
||||
for table in tables:
|
||||
try:
|
||||
signoz.telemetrystore.conn.query(
|
||||
f"ALTER TABLE signoz_metrics.{table} ON CLUSTER '{signoz.telemetrystore.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' REMOVE TTL"
|
||||
f"ALTER TABLE signoz_metrics.{table} ON CLUSTER '{cluster}' REMOVE TTL"
|
||||
)
|
||||
signoz.telemetrystore.conn.query(
|
||||
f"ALTER TABLE signoz_metrics.{table} ON CLUSTER '{signoz.telemetrystore.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER']}' RESET SETTING storage_policy;"
|
||||
f"ALTER TABLE signoz_metrics.{table} ON CLUSTER '{cluster}' RESET SETTING storage_policy;"
|
||||
)
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
print(f"ttl and storage policy reset failed for {table}: {e}")
|
||||
|
||||
331
tests/integration/fixtures/querier.py
Normal file
331
tests/integration/fixtures/querier.py
Normal file
@@ -0,0 +1,331 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
|
||||
DEFAULT_STEP_INTERVAL = 60 # seconds
|
||||
DEFAULT_TOLERANCE = 1e-9
|
||||
QUERY_TIMEOUT = 30 # seconds
|
||||
|
||||
|
||||
def make_query_request(
|
||||
signoz: types.SigNoz,
|
||||
token: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
queries: List[Dict],
|
||||
*,
|
||||
request_type: str = "time_series",
|
||||
format_options: Optional[Dict] = None,
|
||||
variables: Optional[Dict] = None,
|
||||
no_cache: bool = True,
|
||||
timeout: int = QUERY_TIMEOUT,
|
||||
) -> requests.Response:
|
||||
if format_options is None:
|
||||
format_options = {"formatTableResultForUI": False, "fillGaps": False}
|
||||
|
||||
payload = {
|
||||
"schemaVersion": "v1",
|
||||
"start": start_ms,
|
||||
"end": end_ms,
|
||||
"requestType": request_type,
|
||||
"compositeQuery": {"queries": queries},
|
||||
"formatOptions": format_options,
|
||||
"noCache": no_cache,
|
||||
}
|
||||
if variables:
|
||||
payload["variables"] = variables
|
||||
|
||||
return requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v5/query_range"),
|
||||
timeout=timeout,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
json=payload,
|
||||
)
|
||||
|
||||
|
||||
def build_builder_query(
|
||||
name: str,
|
||||
metric_name: str,
|
||||
time_aggregation: str,
|
||||
space_aggregation: str,
|
||||
*,
|
||||
temporality: str = "cumulative",
|
||||
step_interval: int = DEFAULT_STEP_INTERVAL,
|
||||
group_by: Optional[List[str]] = None,
|
||||
filter_expression: Optional[str] = None,
|
||||
functions: Optional[List[Dict]] = None,
|
||||
disabled: bool = False,
|
||||
) -> Dict:
|
||||
spec: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": metric_name,
|
||||
"temporality": temporality,
|
||||
"timeAggregation": time_aggregation,
|
||||
"spaceAggregation": space_aggregation,
|
||||
}
|
||||
],
|
||||
"stepInterval": step_interval,
|
||||
"disabled": disabled,
|
||||
}
|
||||
|
||||
if group_by:
|
||||
spec["groupBy"] = [
|
||||
{
|
||||
"name": label,
|
||||
}
|
||||
for label in group_by
|
||||
]
|
||||
|
||||
if filter_expression:
|
||||
spec["filter"] = {"expression": filter_expression}
|
||||
|
||||
if functions:
|
||||
spec["functions"] = functions
|
||||
|
||||
return {"type": "builder_query", "spec": spec}
|
||||
|
||||
|
||||
def build_formula_query(
|
||||
name: str,
|
||||
expression: str,
|
||||
*,
|
||||
functions: Optional[List[Dict]] = None,
|
||||
disabled: bool = False,
|
||||
) -> Dict:
|
||||
spec: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"expression": expression,
|
||||
"disabled": disabled,
|
||||
}
|
||||
if functions:
|
||||
spec["functions"] = functions
|
||||
return {"type": "builder_formula", "spec": spec}
|
||||
|
||||
|
||||
def build_function(name: str, *args: Any) -> Dict:
|
||||
func: Dict[str, Any] = {"name": name}
|
||||
if args:
|
||||
func["args"] = [{"value": arg} for arg in args]
|
||||
return func
|
||||
|
||||
|
||||
def get_series_values(response_json: Dict, query_name: str) -> List[Dict]:
|
||||
results = response_json.get("data", {}).get("data", {}).get("results", [])
|
||||
result = find_named_result(results, query_name)
|
||||
if not result:
|
||||
return []
|
||||
aggregations = result.get("aggregations", [])
|
||||
if not aggregations:
|
||||
return []
|
||||
# at the time of writing this, the series is always a list with one element
|
||||
series = aggregations[0].get("series", [])
|
||||
if not series:
|
||||
return []
|
||||
return series[0].get("values", [])
|
||||
|
||||
|
||||
def get_all_series(response_json: Dict, query_name: str) -> List[Dict]:
|
||||
results = response_json.get("data", {}).get("data", {}).get("results", [])
|
||||
result = find_named_result(results, query_name)
|
||||
if not result:
|
||||
return []
|
||||
aggregations = result.get("aggregations", [])
|
||||
if not aggregations:
|
||||
return []
|
||||
# at the time of writing this, the series is always a list with one element
|
||||
return aggregations[0].get("series", [])
|
||||
|
||||
|
||||
def get_scalar_value(response_json: Dict, query_name: str) -> Optional[float]:
|
||||
values = get_series_values(response_json, query_name)
|
||||
if values:
|
||||
return values[0].get("value")
|
||||
return None
|
||||
|
||||
|
||||
def compare_values(
|
||||
v1: float,
|
||||
v2: float,
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
) -> bool:
|
||||
return abs(v1 - v2) <= tolerance
|
||||
|
||||
|
||||
def compare_series_values(
|
||||
values1: List[Dict],
|
||||
values2: List[Dict],
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
) -> bool:
|
||||
if len(values1) != len(values2):
|
||||
return False
|
||||
|
||||
sorted1 = sorted(values1, key=lambda x: x["timestamp"])
|
||||
sorted2 = sorted(values2, key=lambda x: x["timestamp"])
|
||||
|
||||
for v1, v2 in zip(sorted1, sorted2):
|
||||
if v1["timestamp"] != v2["timestamp"]:
|
||||
return False
|
||||
if not compare_values(v1["value"], v2["value"], tolerance):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compare_all_series(
|
||||
series1: List[Dict],
|
||||
series2: List[Dict],
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
) -> bool:
|
||||
if len(series1) != len(series2):
|
||||
return False
|
||||
|
||||
# oh my lovely python
|
||||
def series_key(s: Dict) -> str:
|
||||
labels = s.get("labels", [])
|
||||
return str(
|
||||
sorted(
|
||||
[
|
||||
(lbl.get("key", {}).get("name", ""), lbl.get("value", ""))
|
||||
for lbl in labels
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
sorted1 = sorted(series1, key=series_key)
|
||||
sorted2 = sorted(series2, key=series_key)
|
||||
|
||||
for s1, s2 in zip(sorted1, sorted2):
|
||||
if series_key(s1) != series_key(s2):
|
||||
return False
|
||||
if not compare_series_values(
|
||||
s1.get("values", []),
|
||||
s2.get("values", []),
|
||||
tolerance,
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def assert_results_equal(
|
||||
result_cached: Dict,
|
||||
result_no_cache: Dict,
|
||||
query_name: str,
|
||||
context: str,
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
) -> None:
|
||||
values_cached = get_series_values(result_cached, query_name)
|
||||
values_no_cache = get_series_values(result_no_cache, query_name)
|
||||
|
||||
sorted_cached = sorted(values_cached, key=lambda x: x["timestamp"])
|
||||
sorted_no_cache = sorted(values_no_cache, key=lambda x: x["timestamp"])
|
||||
|
||||
assert len(sorted_cached) == len(sorted_no_cache), (
|
||||
f"{context}: Different number of values. "
|
||||
f"Cached: {len(sorted_cached)}, No-cache: {len(sorted_no_cache)}\n"
|
||||
f"Cached timestamps: {[v['timestamp'] for v in sorted_cached]}\n"
|
||||
f"No-cache timestamps: {[v['timestamp'] for v in sorted_no_cache]}"
|
||||
)
|
||||
|
||||
for v_cached, v_no_cache in zip(sorted_cached, sorted_no_cache):
|
||||
assert v_cached["timestamp"] == v_no_cache["timestamp"], (
|
||||
f"{context}: Timestamp mismatch. "
|
||||
f"Cached: {v_cached['timestamp']}, No-cache: {v_no_cache['timestamp']}"
|
||||
)
|
||||
assert compare_values(v_cached["value"], v_no_cache["value"], tolerance), (
|
||||
f"{context}: Value mismatch at timestamp {v_cached['timestamp']}. "
|
||||
f"Cached: {v_cached['value']}, No-cache: {v_no_cache['value']}"
|
||||
)
|
||||
|
||||
|
||||
def assert_all_series_equal(
|
||||
result_cached: Dict,
|
||||
result_no_cache: Dict,
|
||||
query_name: str,
|
||||
context: str,
|
||||
tolerance: float = DEFAULT_TOLERANCE,
|
||||
) -> None:
|
||||
series_cached = get_all_series(result_cached, query_name)
|
||||
series_no_cache = get_all_series(result_no_cache, query_name)
|
||||
|
||||
assert compare_all_series(
|
||||
series_cached, series_no_cache, tolerance
|
||||
), f"{context}: Cached series differ from non-cached series"
|
||||
|
||||
|
||||
def expected_minutely_bucket_timestamps_ms(now: datetime) -> List[List[int]]:
|
||||
previous_five = [
|
||||
int((now - timedelta(minutes=m)).timestamp() * 1000) for m in range(5, 0, -1)
|
||||
]
|
||||
with_current = previous_five + [int(now.timestamp() * 1000)]
|
||||
return [previous_five, with_current]
|
||||
|
||||
|
||||
def assert_minutely_bucket_timestamps(
|
||||
points: List[Dict[str, Any]],
|
||||
now: datetime,
|
||||
*,
|
||||
context: str,
|
||||
) -> List[int]:
|
||||
expected = expected_minutely_bucket_timestamps_ms(now)
|
||||
actual = [p["timestamp"] for p in points]
|
||||
assert actual in expected, f"Unexpected timestamps for {context}: {actual}"
|
||||
return actual
|
||||
|
||||
|
||||
def assert_minutely_bucket_values(
|
||||
points: List[Dict[str, Any]],
|
||||
now: datetime,
|
||||
*,
|
||||
expected_by_ts: Dict[int, float],
|
||||
context: str,
|
||||
) -> None:
|
||||
timestamps = assert_minutely_bucket_timestamps(points, now, context=context)
|
||||
expected = {ts: 0 for ts in timestamps}
|
||||
expected.update(expected_by_ts)
|
||||
|
||||
for point in points:
|
||||
ts = point["timestamp"]
|
||||
assert point["value"] == expected[ts], (
|
||||
f"Unexpected value for {context} at timestamp={ts}: "
|
||||
f"got {point['value']}, expected {expected[ts]}"
|
||||
)
|
||||
|
||||
|
||||
def index_series_by_label(
|
||||
series: List[Dict[str, Any]],
|
||||
label_name: str,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
series_by_label: Dict[str, Dict[str, Any]] = {}
|
||||
for s in series:
|
||||
label = next(
|
||||
(
|
||||
l
|
||||
for l in s.get("labels", [])
|
||||
if l.get("key", {}).get("name") == label_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert label is not None, f"Expected {label_name} label in series"
|
||||
series_by_label[label["value"]] = s
|
||||
return series_by_label
|
||||
|
||||
|
||||
def find_named_result(
|
||||
results: List[Dict[str, Any]],
|
||||
name: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
return next(
|
||||
(
|
||||
r
|
||||
for r in results
|
||||
if r.get("name") == name
|
||||
or r.get("queryName") == name
|
||||
or (r.get("spec") or {}).get("name") == name
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -28,15 +28,14 @@ def test_create_and_delete_dashboard_without_license(
|
||||
dashboard_id = data["id"]
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(
|
||||
f"/api/v1/dashboards/{dashboard_id}"
|
||||
),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/dashboards/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
@@ -49,7 +48,6 @@ def test_apply_license(
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
|
||||
def test_create_and_delete_dashboard_with_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
@@ -70,11 +68,9 @@ def test_create_and_delete_dashboard_with_license(
|
||||
dashboard_id = data["id"]
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_configs["8080"].get(
|
||||
f"/api/v1/dashboards/{dashboard_id}"
|
||||
),
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/dashboards/{dashboard_id}"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
@@ -7,7 +7,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from src.querier.timeseries_utils import (
|
||||
from fixtures.querier import (
|
||||
assert_minutely_bucket_values,
|
||||
find_named_result,
|
||||
index_series_by_label,
|
||||
|
||||
@@ -7,7 +7,7 @@ import requests
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from src.querier.timeseries_utils import (
|
||||
from fixtures.querier import (
|
||||
assert_minutely_bucket_values,
|
||||
find_named_result,
|
||||
index_series_by_label,
|
||||
|
||||
@@ -6,12 +6,12 @@ import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
from src.querier.timeseries_utils import (
|
||||
from fixtures.querier import (
|
||||
assert_minutely_bucket_values,
|
||||
find_named_result,
|
||||
index_series_by_label,
|
||||
)
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
|
||||
|
||||
def test_traces_list(
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Look at the cumulative_counters_1h.jsonl file for the relevant data
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Callable, List
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
build_builder_query,
|
||||
get_all_series,
|
||||
get_series_values,
|
||||
make_query_request,
|
||||
)
|
||||
|
||||
TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "testdata")
|
||||
CUMULATIVE_COUNTERS_FILE = os.path.join(TESTDATA_DIR, "cumulative_counters_1h.jsonl")
|
||||
|
||||
|
||||
def test_rate_with_steady_values_and_reset(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
metric_name = "test_rate_stale"
|
||||
|
||||
metrics = Metrics.load_from_file(
|
||||
CUMULATIVE_COUNTERS_FILE,
|
||||
base_time=now - timedelta(minutes=60),
|
||||
metric_name_override=metric_name,
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
"rate",
|
||||
"sum",
|
||||
temporality="cumulative",
|
||||
filter_expression='endpoint = "/orders"',
|
||||
)
|
||||
|
||||
response = make_query_request(signoz, token, start_ms, end_ms, [query])
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
data = response.json()
|
||||
result_values = sorted(get_series_values(data, "A"), key=lambda x: x["timestamp"])
|
||||
assert len(result_values) >= 59
|
||||
# the counter reset happened at 31st minute
|
||||
assert (
|
||||
result_values[30]["value"] == 0.0167
|
||||
) # i.e 2/120 i.e 29th to 31st minute changes
|
||||
assert (
|
||||
result_values[31]["value"] == 0.133
|
||||
) # i.e 10/60 i.e 31st to 32nd minute changes
|
||||
count_of_steady_rate = sum(1 for v in result_values if v["value"] == 0.0833)
|
||||
assert (
|
||||
count_of_steady_rate >= 56
|
||||
) # 59 - (1 reset + 1 high rate + 1 at the beginning)
|
||||
# All rates should be non-negative (stale periods = 0 rate)
|
||||
for v in result_values:
|
||||
assert v["value"] >= 0, f"Rate should not be negative: {v['value']}"
|
||||
|
||||
|
||||
def test_rate_group_by_endpoint(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
metric_name = "test_rate_groupby"
|
||||
|
||||
metrics = Metrics.load_from_file(
|
||||
CUMULATIVE_COUNTERS_FILE,
|
||||
base_time=now - timedelta(minutes=60),
|
||||
metric_name_override=metric_name,
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
"rate",
|
||||
"sum",
|
||||
temporality="cumulative",
|
||||
group_by=["endpoint"],
|
||||
)
|
||||
|
||||
response = make_query_request(signoz, token, start_ms, end_ms, [query])
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
data = response.json()
|
||||
all_series = get_all_series(data, "A")
|
||||
|
||||
# Should have 5 different endpoints
|
||||
assert (
|
||||
len(all_series) == 5
|
||||
), f"Expected 5 series for 5 endpoints, got {len(all_series)}"
|
||||
|
||||
# endpoint -> values
|
||||
endpoint_values = {}
|
||||
for series in all_series:
|
||||
endpoint = series.get("labels", [{}])[0].get("value", "unknown")
|
||||
values = sorted(series.get("values", []), key=lambda x: x["timestamp"])
|
||||
endpoint_values[endpoint] = values
|
||||
|
||||
expected_endpoints = {"/products", "/health", "/checkout", "/orders", "/users"}
|
||||
assert (
|
||||
set(endpoint_values.keys()) == expected_endpoints
|
||||
), f"Expected endpoints {expected_endpoints}, got {set(endpoint_values.keys())}"
|
||||
|
||||
# at no point rate should be negative
|
||||
for endpoint, values in endpoint_values.items():
|
||||
for v in values:
|
||||
assert (
|
||||
v["value"] >= 0
|
||||
), f"Rate for {endpoint} should not be negative: {v['value']}"
|
||||
|
||||
# /health: 60 data points (t01-t60), steady +10/min
|
||||
# rate = 10/60 = 0.167
|
||||
health_values = endpoint_values["/health"]
|
||||
assert (
|
||||
len(health_values) >= 58
|
||||
), f"Expected >= 58 values for /health, got {len(health_values)}"
|
||||
count_steady_health = sum(1 for v in health_values if v["value"] == 0.167)
|
||||
assert (
|
||||
count_steady_health >= 57
|
||||
), f"Expected >= 57 steady rate values (0.167) for /health, got {count_steady_health}"
|
||||
# all /health rates should be 0.167 except possibly first/last due to boundaries
|
||||
for v in health_values[1:-1]:
|
||||
assert v["value"] == 0.167, f"Expected /health rate 0.167, got {v['value']}"
|
||||
|
||||
# /products: 51 data points with 10-minute gap (t20-t29 missing), steady +20/min
|
||||
# rate = 20/60 = 0.333, gap causes lower averaged rate at boundary
|
||||
products_values = endpoint_values["/products"]
|
||||
assert (
|
||||
len(products_values) >= 49
|
||||
), f"Expected >= 49 values for /products, got {len(products_values)}"
|
||||
count_steady_products = sum(1 for v in products_values if v["value"] == 0.333)
|
||||
|
||||
# most values should be 0.333, some boundary values differ due to 10-min gap
|
||||
assert (
|
||||
count_steady_products >= 46
|
||||
), f"Expected >= 46 steady rate values (0.333) for /products, got {count_steady_products}"
|
||||
|
||||
# check that non-0.333 values are due to gap averaging (should be lower)
|
||||
gap_boundary_values = [v["value"] for v in products_values if v["value"] != 0.333]
|
||||
for val in gap_boundary_values:
|
||||
assert (
|
||||
0 < val < 0.333
|
||||
), f"Gap boundary values should be between 0 and 0.333, got {val}"
|
||||
|
||||
# /checkout: 61 data points (t00-t60), +1/min normal, +50/min spike at t40-t44
|
||||
# normal rate = 1/60 = 0.0167, spike rate = 50/60 = 0.833
|
||||
checkout_values = endpoint_values["/checkout"]
|
||||
assert (
|
||||
len(checkout_values) >= 59
|
||||
), f"Expected >= 59 values for /checkout, got {len(checkout_values)}"
|
||||
count_steady_checkout = sum(1 for v in checkout_values if v["value"] == 0.0167)
|
||||
assert (
|
||||
count_steady_checkout >= 53
|
||||
), f"Expected >= 53 steady rate values (0.0167) for /checkout, got {count_steady_checkout}"
|
||||
# check that spike values exist (traffic spike +50/min at t40-t44)
|
||||
count_spike_checkout = sum(1 for v in checkout_values if v["value"] == 0.833)
|
||||
assert (
|
||||
count_spike_checkout >= 4
|
||||
), f"Expected >= 4 spike rate values (0.833) for /checkout, got {count_spike_checkout}"
|
||||
|
||||
# spike values should be consecutive
|
||||
spike_indices = [
|
||||
i for i, v in enumerate[Any](checkout_values) if v["value"] == 0.833
|
||||
]
|
||||
assert len(spike_indices) >= 4, f"Expected >= 4 spike indices, got {spike_indices}"
|
||||
# consecutiveness
|
||||
for i in range(1, len(spike_indices)):
|
||||
assert (
|
||||
spike_indices[i] == spike_indices[i - 1] + 1
|
||||
), f"Spike indices should be consecutive, got {spike_indices}"
|
||||
|
||||
# /orders: 60 data points (t00-t60) with gap at t30, counter reset at t31 (150->2)
|
||||
# rate = 5/60 = 0.0833
|
||||
# reset at t31 causes: rate at t30 includes gap (lower), t31 has high rate after reset
|
||||
orders_values = endpoint_values["/orders"]
|
||||
assert (
|
||||
len(orders_values) >= 58
|
||||
), f"Expected >= 58 values for /orders, got {len(orders_values)}"
|
||||
count_steady_orders = sum(1 for v in orders_values if v["value"] == 0.0833)
|
||||
assert (
|
||||
count_steady_orders >= 55
|
||||
), f"Expected >= 55 steady rate values (0.0833) for /orders, got {count_steady_orders}"
|
||||
# check for counter reset effects - there should be some non-standard values
|
||||
non_standard_orders = [v["value"] for v in orders_values if v["value"] != 0.0833]
|
||||
assert (
|
||||
len(non_standard_orders) >= 2
|
||||
), f"Expected >= 2 non-standard values due to counter reset, got {non_standard_orders}"
|
||||
# post-reset value should be higher (new counter value / interval)
|
||||
high_rate_orders = [v for v in non_standard_orders if v > 0.0833]
|
||||
assert (
|
||||
len(high_rate_orders) >= 1
|
||||
), f"Expected at least one high rate value after counter reset, got {non_standard_orders}"
|
||||
|
||||
# /users: 56 data points (t05-t60), sparse +1 every 5 minutes
|
||||
# Rate = 1/60 = 0.0167 during increment, 0 during flat periods
|
||||
users_values = endpoint_values["/users"]
|
||||
assert (
|
||||
len(users_values) >= 54
|
||||
), f"Expected >= 54 values for /users, got {len(users_values)}"
|
||||
count_zero_users = sum(1 for v in users_values if v["value"] == 0)
|
||||
# most values should be 0 (flat periods between increments)
|
||||
assert (
|
||||
count_zero_users >= 40
|
||||
), f"Expected >= 40 zero rate values for /users (sparse data), got {count_zero_users}"
|
||||
# non-zero values should be 0.0167 (1/60 increment rate)
|
||||
non_zero_users = [v["value"] for v in users_values if v["value"] != 0]
|
||||
count_increment_rate = sum(1 for v in non_zero_users if v == 0.0167)
|
||||
assert (
|
||||
count_increment_rate >= 8
|
||||
), f"Expected >= 8 increment rate values (0.0167) for /users, got {count_increment_rate}"
|
||||
@@ -1,76 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def expected_minutely_bucket_timestamps_ms(now: datetime) -> List[List[int]]:
|
||||
previous_five = [
|
||||
int((now - timedelta(minutes=m)).timestamp() * 1000) for m in range(5, 0, -1)
|
||||
]
|
||||
with_current = previous_five + [int(now.timestamp() * 1000)]
|
||||
return [previous_five, with_current]
|
||||
|
||||
|
||||
def assert_minutely_bucket_timestamps(
|
||||
points: List[Dict[str, Any]],
|
||||
now: datetime,
|
||||
*,
|
||||
context: str,
|
||||
) -> List[int]:
|
||||
expected = expected_minutely_bucket_timestamps_ms(now)
|
||||
actual = [p["timestamp"] for p in points]
|
||||
assert actual in expected, f"Unexpected timestamps for {context}: {actual}"
|
||||
return actual
|
||||
|
||||
|
||||
def assert_minutely_bucket_values(
|
||||
points: List[Dict[str, Any]],
|
||||
now: datetime,
|
||||
*,
|
||||
expected_by_ts: Dict[int, float],
|
||||
context: str,
|
||||
) -> None:
|
||||
timestamps = assert_minutely_bucket_timestamps(points, now, context=context)
|
||||
expected = {ts: 0 for ts in timestamps}
|
||||
expected.update(expected_by_ts)
|
||||
|
||||
for point in points:
|
||||
ts = point["timestamp"]
|
||||
assert point["value"] == expected[ts], (
|
||||
f"Unexpected value for {context} at timestamp={ts}: "
|
||||
f"got {point['value']}, expected {expected[ts]}"
|
||||
)
|
||||
|
||||
|
||||
def index_series_by_label(
|
||||
series: List[Dict[str, Any]],
|
||||
label_name: str,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
series_by_label: Dict[str, Dict[str, Any]] = {}
|
||||
for s in series:
|
||||
label = next(
|
||||
(
|
||||
l
|
||||
for l in s.get("labels", [])
|
||||
if l.get("key", {}).get("name") == label_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert label is not None, f"Expected {label_name} label in series"
|
||||
series_by_label[label["value"]] = s
|
||||
return series_by_label
|
||||
|
||||
|
||||
def find_named_result(
|
||||
results: List[Dict[str, Any]],
|
||||
name: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
return next(
|
||||
(
|
||||
r
|
||||
for r in results
|
||||
if r.get("name") == name
|
||||
or r.get("queryName") == name
|
||||
or (r.get("spec") or {}).get("name") == name
|
||||
),
|
||||
None,
|
||||
)
|
||||
288
tests/integration/testdata/cumulative_counters_1h.jsonl
vendored
Normal file
288
tests/integration/testdata/cumulative_counters_1h.jsonl
vendored
Normal file
@@ -0,0 +1,288 @@
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:01:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:02:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:03:00+00:00","value":30,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:04:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:05:00+00:00","value":50,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:06:00+00:00","value":60,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:07:00+00:00","value":70,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:08:00+00:00","value":80,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:09:00+00:00","value":90,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:10:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:11:00+00:00","value":110,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:12:00+00:00","value":120,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:13:00+00:00","value":130,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:14:00+00:00","value":140,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:15:00+00:00","value":150,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:16:00+00:00","value":160,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:17:00+00:00","value":170,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:18:00+00:00","value":180,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:19:00+00:00","value":190,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:20:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:21:00+00:00","value":210,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:22:00+00:00","value":220,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:23:00+00:00","value":230,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:24:00+00:00","value":240,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:25:00+00:00","value":250,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:26:00+00:00","value":260,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:27:00+00:00","value":270,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:28:00+00:00","value":280,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:29:00+00:00","value":290,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:30:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:31:00+00:00","value":310,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:32:00+00:00","value":320,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:33:00+00:00","value":330,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:34:00+00:00","value":340,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:35:00+00:00","value":350,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:36:00+00:00","value":360,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:37:00+00:00","value":370,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:38:00+00:00","value":380,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:39:00+00:00","value":390,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:40:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:41:00+00:00","value":410,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:42:00+00:00","value":420,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:43:00+00:00","value":430,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:44:00+00:00","value":440,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:45:00+00:00","value":450,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:46:00+00:00","value":460,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:47:00+00:00","value":470,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:48:00+00:00","value":480,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:49:00+00:00","value":490,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:50:00+00:00","value":500,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:51:00+00:00","value":510,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:52:00+00:00","value":520,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:53:00+00:00","value":530,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:54:00+00:00","value":540,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:55:00+00:00","value":550,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:56:00+00:00","value":560,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:57:00+00:00","value":570,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:58:00+00:00","value":580,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T10:59:00+00:00","value":590,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/health","status_code":"200"},"timestamp":"2025-01-10T11:00:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:05:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:06:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:07:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:08:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:09:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:10:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:11:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:12:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:13:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:14:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:15:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:16:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:17:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:18:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:19:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:20:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:21:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:22:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:23:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:24:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:25:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:26:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:27:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:28:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:29:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:30:00+00:00","value":6,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:31:00+00:00","value":6,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:32:00+00:00","value":6,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:33:00+00:00","value":6,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:34:00+00:00","value":6,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:35:00+00:00","value":7,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:36:00+00:00","value":7,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:37:00+00:00","value":7,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:38:00+00:00","value":7,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:39:00+00:00","value":7,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:40:00+00:00","value":8,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:41:00+00:00","value":8,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:42:00+00:00","value":8,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:43:00+00:00","value":8,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:44:00+00:00","value":8,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:45:00+00:00","value":9,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:46:00+00:00","value":9,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:47:00+00:00","value":9,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:48:00+00:00","value":9,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:49:00+00:00","value":9,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:50:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:51:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:52:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:53:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:54:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:55:00+00:00","value":11,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:56:00+00:00","value":11,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:57:00+00:00","value":11,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:58:00+00:00","value":11,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T10:59:00+00:00","value":11,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/users","status_code":"500"},"timestamp":"2025-01-10T11:00:00+00:00","value":12,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:00:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:01:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:02:00+00:00","value":15,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:03:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:04:00+00:00","value":25,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:05:00+00:00","value":30,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:06:00+00:00","value":35,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:07:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:08:00+00:00","value":45,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:09:00+00:00","value":50,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:10:00+00:00","value":55,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:11:00+00:00","value":60,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:12:00+00:00","value":65,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:13:00+00:00","value":70,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:14:00+00:00","value":75,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:15:00+00:00","value":80,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:16:00+00:00","value":85,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:17:00+00:00","value":90,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:18:00+00:00","value":95,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:19:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:20:00+00:00","value":105,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:21:00+00:00","value":110,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:22:00+00:00","value":115,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:23:00+00:00","value":120,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:24:00+00:00","value":125,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:25:00+00:00","value":130,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:26:00+00:00","value":135,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:27:00+00:00","value":140,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:28:00+00:00","value":145,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:29:00+00:00","value":150,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:31:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:32:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:33:00+00:00","value":15,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:34:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:35:00+00:00","value":25,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:36:00+00:00","value":30,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:37:00+00:00","value":35,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:38:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:39:00+00:00","value":45,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:40:00+00:00","value":50,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:41:00+00:00","value":55,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:42:00+00:00","value":60,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:43:00+00:00","value":65,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:44:00+00:00","value":70,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:45:00+00:00","value":75,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:46:00+00:00","value":80,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:47:00+00:00","value":85,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:48:00+00:00","value":90,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:49:00+00:00","value":95,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:50:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:51:00+00:00","value":105,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:52:00+00:00","value":110,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:53:00+00:00","value":115,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:54:00+00:00","value":120,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:55:00+00:00","value":125,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:56:00+00:00","value":130,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:57:00+00:00","value":135,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:58:00+00:00","value":140,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T10:59:00+00:00","value":145,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"api","endpoint":"/orders","status_code":"200"},"timestamp":"2025-01-10T11:00:00+00:00","value":150,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:00:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:01:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:02:00+00:00","value":60,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:03:00+00:00","value":80,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:04:00+00:00","value":100,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:05:00+00:00","value":120,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:06:00+00:00","value":140,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:07:00+00:00","value":160,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:08:00+00:00","value":180,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:09:00+00:00","value":200,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:10:00+00:00","value":220,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:11:00+00:00","value":240,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:12:00+00:00","value":260,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:13:00+00:00","value":280,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:14:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:15:00+00:00","value":320,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:16:00+00:00","value":340,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:17:00+00:00","value":360,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:18:00+00:00","value":380,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:19:00+00:00","value":400,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:30:00+00:00","value":420,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:31:00+00:00","value":440,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:32:00+00:00","value":460,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:33:00+00:00","value":480,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:34:00+00:00","value":500,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:35:00+00:00","value":520,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:36:00+00:00","value":540,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:37:00+00:00","value":560,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:38:00+00:00","value":580,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:39:00+00:00","value":600,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:40:00+00:00","value":620,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:41:00+00:00","value":640,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:42:00+00:00","value":660,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:43:00+00:00","value":680,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:44:00+00:00","value":700,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:45:00+00:00","value":720,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:46:00+00:00","value":740,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:47:00+00:00","value":760,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:48:00+00:00","value":780,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:49:00+00:00","value":800,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:50:00+00:00","value":820,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:51:00+00:00","value":840,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:52:00+00:00","value":860,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:53:00+00:00","value":880,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:54:00+00:00","value":900,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:55:00+00:00","value":920,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:56:00+00:00","value":940,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:57:00+00:00","value":960,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:58:00+00:00","value":980,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T10:59:00+00:00","value":1000,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/products","status_code":"200"},"timestamp":"2025-01-10T11:00:00+00:00","value":1020,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:00:00+00:00","value":1,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:01:00+00:00","value":2,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:02:00+00:00","value":3,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:03:00+00:00","value":4,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:04:00+00:00","value":5,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:05:00+00:00","value":6,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:06:00+00:00","value":7,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:07:00+00:00","value":8,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:08:00+00:00","value":9,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:09:00+00:00","value":10,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:10:00+00:00","value":11,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:11:00+00:00","value":12,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:12:00+00:00","value":13,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:13:00+00:00","value":14,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:14:00+00:00","value":15,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:15:00+00:00","value":16,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:16:00+00:00","value":17,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:17:00+00:00","value":18,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:18:00+00:00","value":19,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:19:00+00:00","value":20,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:20:00+00:00","value":21,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:21:00+00:00","value":22,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:22:00+00:00","value":23,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:23:00+00:00","value":24,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:24:00+00:00","value":25,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:25:00+00:00","value":26,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:26:00+00:00","value":27,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:27:00+00:00","value":28,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:28:00+00:00","value":29,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:29:00+00:00","value":30,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:30:00+00:00","value":31,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:31:00+00:00","value":32,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:32:00+00:00","value":33,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:33:00+00:00","value":34,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:34:00+00:00","value":35,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:35:00+00:00","value":36,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:36:00+00:00","value":37,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:37:00+00:00","value":38,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:38:00+00:00","value":39,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:39:00+00:00","value":40,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:40:00+00:00","value":90,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:41:00+00:00","value":140,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:42:00+00:00","value":190,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:43:00+00:00","value":240,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:44:00+00:00","value":290,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:45:00+00:00","value":291,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:46:00+00:00","value":292,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:47:00+00:00","value":293,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:48:00+00:00","value":294,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:49:00+00:00","value":295,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:50:00+00:00","value":296,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:51:00+00:00","value":297,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:52:00+00:00","value":298,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:53:00+00:00","value":299,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:54:00+00:00","value":300,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:55:00+00:00","value":301,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:56:00+00:00","value":302,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:57:00+00:00","value":303,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:58:00+00:00","value":304,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T10:59:00+00:00","value":305,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
{"metric_name":"http.request.count","labels":{"service":"web","endpoint":"/checkout","status_code":"429"},"timestamp":"2025-01-10T11:00:00+00:00","value":306,"temporality":"Cumulative","type_":"Sum","is_monotonic":true,"flags":0,"description":"","unit":"","env":"default","resource_attrs":{},"scope_attrs":{}}
|
||||
Reference in New Issue
Block a user