Compare commits

...

40 Commits

Author SHA1 Message Date
Nikhil Soni
18b542b8c5 fix: use absolute path for commit smg lint
If project dir is present inside another repo in user's local
setup, using relative path can create problem in finding the
.git path since this command also changes the directory to
frontend.
Also removes --edit flag since it's already present in
yarn command commitlint in frontend/package.json
2026-01-27 13:51:54 +05:30
Nikhil Soni
8a8550ac85 fix: change clickhouse volume path on host to docker
Clickhouse tries to own the /var/lib/clickhouse dir and
if it's mounted on a host machine path, the chown command
in clickhouse startup fails with below error:
chown: changing ownership of '/var/lib/clickhouse/': Permission denied

This change uses docker managed volume where permissions are auto managed.
I'm using colima instead of docker desktop on mac, so problem might
be specific to colima setup but fix should be generic.
https://github.com/abiosoft/colima
2026-01-27 13:51:54 +05:30
Yunus M
8629c959f0 chore: move types, constants to separate files, delete unused code (#10026)
* chore: move types, constants to separate files, delete unused code

* chore: fix import error
2026-01-21 08:42:11 +00:00
Yunus M
10760e6e1b Update pull_request_template.md (#10064)
Update PR template to include 

Before / After Screenshots
Issues closed by PR
2026-01-21 13:52:15 +05:30
primus-bot[bot]
4f45645b32 chore(release): bump to v0.108.0 (#10065)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-01-21 12:13:18 +05:30
Karan Balani
1417e22ae4 fix: use reliable selenium methods for actions in integration tests (#10061) 2026-01-21 06:09:42 +00:00
Pandey
3051d442c0 fix: move ee references out of cmd/community (#10063)
- move ee references out of cmd/community
- add check in commitci
2026-01-21 09:22:40 +05:30
Karan Balani
ea15ce4e04 feat: sso stats reporting (#10062)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-01-20 18:57:35 +00:00
Ashwin Bhatkal
865a7a5a31 fix: promql and clickhouse query based panels refresh on dynamic variable change (#10060)
* fix: promql and clickhouse query based panels refresh on dynamic variable change

* chore: add test
2026-01-20 17:19:58 +00:00
swapnil-signoz
de4ca50a40 refactor: using global config's ingestion URL (#10019)
* refactor: using global config's ingestion URL

* refactor: add global ingestion URL to configuration

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2026-01-20 17:05:56 +00:00
Amlan Kumar Nandy
8cabaafc58 fix: handle threshold unit scaling issues (#10020) 2026-01-20 16:22:49 +00:00
Ashwin Bhatkal
e9d66b8094 chore: update yarn.lock (#10051) 2026-01-20 16:07:05 +00:00
Karan Balani
26d3d6b1e4 feat: gateway apis (#10010) 2026-01-20 15:46:46 +00:00
Ashwin Bhatkal
36d6debeab chore: update .gitignore with only settings.json of .vscode folder (#10058) 2026-01-20 13:54:58 +00:00
Pandey
445b0cace8 chore: add codeowners for scaffold (#10055) 2026-01-20 12:19:14 +00:00
Pandey
132f10f8a3 feat(binding): add support for query params (#10053)
- add support for query params in the binding package.
2026-01-20 11:59:12 +00:00
Srikanth Chekuri
14011bc277 fix: do not sort in descending locally if the other is explicitly spe… (#10033) 2026-01-20 11:17:08 +00:00
aniketio-ctrl
f17a332c23 feat(license-section): add section to view license key (#10039)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(license-section): added section to view license key

* feat(license-key): add license key page

* feat(license-section): added section to view license key

* feat(license-section): added section to view license key

* feat(license-section): added section to view license key

* feat(license-section): added section to view license key

* feat(license-section): resoved comments

* feat(license-section): resoved fmt error
2026-01-20 16:05:57 +05:30
Aditya Singh
5ae7a464e6 Fix Cmd/ctrl + Enter in qb to run query (#10048)
* feat: cmd enter in qb

* feat: cnd enter test added

* feat: update test case

* feat: update test case

* feat: minor refactor

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-01-20 13:30:54 +05:30
Pandey
51c3628f6e fix(signoz): remove version check at the beginning (#10046) 2026-01-20 06:56:06 +00:00
Aditya Singh
6a69076828 Add attribute action in common col of span details drawer (#10017)
* feat: add attribute action in common col of span details drawer

* feat: added test case
2026-01-20 05:44:31 +00:00
Aditya Singh
edd04e2f07 fix: fix auto collapse fields when emptied (#9988)
* fix: fix auto collapse fields when emptied

* fix: revert mocks

* fix: minor fix

* fix: add check for key count change to setSelectedView

* feat: revert change

* feat: instead of count check actual keys
2026-01-20 05:32:13 +00:00
Srikanth Chekuri
ee734cf78c chore: return original error message with hints for invalid promql query (#10034)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
one step towards better experience for https://github.com/SigNoz/signoz/issues/9764
2026-01-20 03:04:59 +05:30
Karan Balani
6d137bcdff feat: idp attributes mapping (#9841) 2026-01-19 22:27:21 +05:30
Ashwin Bhatkal
444161671d fix: query variable referencing custom variable (#10023)
* fix: custom variable to query mapping

* chore: add test
2026-01-19 13:52:58 +00:00
Ashwin Bhatkal
31e9e896ec fix: dashboard - textbox default variable not working (#9843) 2026-01-19 18:23:17 +05:30
SagarRajput-7
325974292f feat: auth revamp flow (#9901)
* feat: auth revamp base setup

* feat: auth revamp signin changes

* feat: auth revamp signup changes

* feat: auth revamp reset password changes

* feat: auth revamp light mode changes

* feat: clean up and added get help link

* feat: cleanup and addressed comments

* feat: cleanup

* feat: comment addressed

* feat: auth revamp for onboarding flow (#9915)

* feat: auth revamp for onboarding flow

* feat: updated invite team member step

* feat: updated light mode

* feat: comment addressed

* feat: added onboarding flow test cases

* feat: fixed feedbacks

* feat: resolved comments and refactoring

* feat: resolved comments and refactoring

* fix: svg import fix

* feat: added test cases for the new auth flow (#9914)

* feat: added test cases for the new auth flow

* feat: updated test cases

* feat: updated test cases

* fix: updated test cases

* fix: updated test cases

* fix: use error content for auth revamp error

* feat: refactor and cleanup

* feat: refactor and cleanup

* fix: add shake animation and light mode fix

* feat: feedback resolved

* feat: feedback resolved

* feat: updated test cases

* feat: feedback resolved

* feat: shake animation update
2026-01-19 13:40:37 +05:30
Vishal Sharma
7c1a531d01 Add context7.json with URL and public key (#10037) 2026-01-19 11:35:37 +05:30
Vikrant Gupta
5a45532a72 chore: update codeowners file (#10031) 2026-01-18 22:00:38 +05:30
Srikanth Chekuri
e9501d2e0f test(integration): add cumulative counter rate tests (#9976) 2026-01-18 16:13:41 +05:30
Srikanth Chekuri
c306e66bcd chore: update CODEOWNERS (#10028) 2026-01-18 15:42:46 +05:30
Yunus M
767a0cc28e chore: improve pull request template clarity and visibility (#10021) 2026-01-16 16:17:17 +00:00
Ashwin Bhatkal
4f9efcc133 chore: remove airbnb from ESLint, update prettier rules and update VS Code settings (#10013)
* chore: remove airbnb from ESLint, update prettier rules and update VS Code settings

* fix: lint errors
2026-01-16 12:05:23 +00:00
Amlan Kumar Nandy
bf2dd612e0 chore: fill y axis unit in alerts coming from dashboards (#9982) 2026-01-16 09:37:16 +00:00
Aditya Singh
d3f15022a4 fix: overflowing tag input on antd (#10018) 2026-01-16 09:15:22 +00:00
Pandey
a5c021e96c fix(kafka): fix nil pointer error in evaluation api (#10015) 2026-01-16 02:24:20 +05:30
Abhi kumar
8b9fcae0cb fix: added layout fix for changelog modal (#10012) 2026-01-15 12:30:10 +00:00
Abhi kumar
e75abcb108 Fix/animation offload to cpu (#9961)
* fix: transition is getting offloaded to cpu because gpu can't handle it in composition

* chore: changed transition from all to transform only
2026-01-15 13:43:44 +05:30
primus-bot[bot]
ffa15f9ec1 chore(release): bump to v0.107.0 (#10007)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-01-15 12:18:52 +05:30
Ishan
87562f5387 feat: onGroupBy - show timeseries panel - #3403 (#9921)
* feat: onGroupBy - show timeseries

* feat: updated testcases for tableview actions

* feat: updated pipeline file to remove groupBy as not used

* feat: updated better code checks

* feat: updated better code checks

* feat: updated tableview actions to update query

* feat: updated types and test cases

* feat: updated dataType (noramlized)
2026-01-15 08:53:08 +05:30
262 changed files with 13714 additions and 3210 deletions

View File

@@ -5,8 +5,8 @@ services:
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
- ${PWD}/fs/etc/clickhouse-server/users.d/users.xml:/etc/clickhouse-server/users.d/users.xml
- ${PWD}/fs/tmp/var/lib/clickhouse/:/var/lib/clickhouse/
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
- clickhouse_data:/var/lib/clickhouse/
ports:
- '127.0.0.1:8123:8123'
- '127.0.0.1:9000:9000'
@@ -69,3 +69,5 @@ services:
schema-migrator-sync:
condition: service_completed_successfully
restart: on-failure
volumes:
clickhouse_data:

59
.github/CODEOWNERS vendored
View File

@@ -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,60 @@
/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
/ee/authz/ @vikrantgupta25
/pkg/authn/ @vikrantgupta25
/ee/authn/ @vikrantgupta25
/pkg/modules/user/ @vikrantgupta25
/pkg/modules/session/ @vikrantgupta25
/pkg/modules/organization/ @vikrantgupta25
/pkg/modules/authdomain/ @vikrantgupta25
/pkg/modules/role/ @vikrantgupta25
# Integration tests
/tests/integration/ @therealpandey
/tests/integration/ @vikrantgupta25
# Dashboard Owners

View File

@@ -1,86 +1,85 @@
## 📄 Summary
<!-- Describe the purpose of the PR in a few sentences. What does it fix/add/update? -->
## Pull Request
---
## ✅ Changes
### 📄 Summary
> Why does this change exist?
> What problem does it solve, and why is this the right approach?
- [ ] Feature: Brief description
- [ ] Bug fix: Brief description
#### Screenshots / Screen Recordings (if applicable)
> Include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. This helps reviewers quickly understand the impact and verify the update.
#### Issues closed by this PR
> Reference issues using `Closes #issue-number` to enable automatic closure on merge.
---
### ✅ 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 -->
---

View File

@@ -25,3 +25,10 @@ jobs:
else
echo "No references to 'ee' packages found in 'pkg' directory"
fi
if grep -R --include="*.go" '.*/ee/.*' cmd/community/; then
echo "Error: Found references to 'ee' packages in 'cmd/community' directory"
exit 1
else
echo "No references to 'ee' packages found in 'cmd/community' directory"
fi

View File

@@ -84,8 +84,11 @@ jobs:
sudo rm /etc/apt/sources.list.d/google-chrome.list
export CHROMEDRIVER_VERSION=`curl -s https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}`
curl -L -O "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip"
unzip chromedriver-linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin
unzip chromedriver-linux64.zip
chmod +x chromedriver-linux64/chromedriver
sudo mv chromedriver-linux64/chromedriver /usr/local/bin/chromedriver
chromedriver -version
google-chrome-stable --version
- name: run
run: |
cd tests/integration && \

6
.gitignore vendored
View File

@@ -1,6 +1,9 @@
node_modules
.vscode
!.vscode/settings.json
deploy/docker/environment_tiny/common_test
frontend/node_modules
frontend/.pnp
@@ -12,7 +15,6 @@ frontend/coverage
# production
frontend/build
frontend/.vscode
frontend/.yarnclean
frontend/.temp_cache
frontend/test-results
@@ -31,7 +33,6 @@ frontend/src/constants/env.ts
.idea
**/.vscode
**/build
**/storage
**/locust-scripts/__pycache__/
@@ -106,7 +107,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/

9
.vscode/settings.json vendored Normal file
View 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
}

View File

@@ -86,7 +86,7 @@ go-run-enterprise: ## Runs the enterprise go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_JWT_SECRET=secret \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \
@@ -103,7 +103,7 @@ go-run-community: ## Runs the community go backend server
@SIGNOZ_INSTRUMENTATION_LOGS_LEVEL=debug \
SIGNOZ_SQLSTORE_SQLITE_PATH=signoz.db \
SIGNOZ_WEB_ENABLED=false \
SIGNOZ_JWT_SECRET=secret \
SIGNOZ_TOKENIZER_JWT_SECRET=secret \
SIGNOZ_ALERTMANAGER_PROVIDER=signoz \
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse \
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://127.0.0.1:9000 \

View File

@@ -5,13 +5,14 @@ import (
"log/slog"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
@@ -24,7 +25,6 @@ import (
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -57,13 +57,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
// print the version
version.Info.PrettyPrint(config.Version)
// add enterprise sqlstore factories to the community sqlstore factories
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
return err
}
signoz, err := signoz.New(
ctx,
config,
@@ -90,6 +83,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Module, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err)

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
@@ -22,6 +23,7 @@ import (
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
@@ -120,6 +122,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, role, queryParser, querier, licensing)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", "error", err)

4
context7.json Normal file
View File

@@ -0,0 +1,4 @@
{
"url": "https://context7.com/signoz/signoz",
"public_key": "pk_6g9GfjdkuPEIDuTGAxnol"
}

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.108.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -195,7 +195,7 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm
- SIGNOZ_JWT_SECRET=secret
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
- DOT_METRICS_ENABLED=true
healthcheck:
test:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.108.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.108.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.108.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -2067,6 +2067,361 @@ paths:
summary: Get features
tags:
- features
/api/v2/gateway/ingestion_keys:
get:
deprecated: false
description: This endpoint returns the ingestion keys for a workspace
operationId: GetIngestionKeys
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
status:
type: string
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get ingestion keys for workspace
tags:
- gateway
post:
deprecated: false
description: This endpoint creates an ingestion key for the workspace
operationId: CreateIngestionKey
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GatewaytypesPostableIngestionKey'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKey'
status:
type: string
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create ingestion key for workspace
tags:
- gateway
/api/v2/gateway/ingestion_keys/{keyId}:
delete:
deprecated: false
description: This endpoint deletes an ingestion key for the workspace
operationId: DeleteIngestionKey
parameters:
- in: path
name: keyId
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete ingestion key for workspace
tags:
- gateway
patch:
deprecated: false
description: This endpoint updates an ingestion key for the workspace
operationId: UpdateIngestionKey
parameters:
- in: path
name: keyId
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GatewaytypesPostableIngestionKey'
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update ingestion key for workspace
tags:
- gateway
/api/v2/gateway/ingestion_keys/{keyId}/limits:
post:
deprecated: false
description: This endpoint creates an ingestion key limit
operationId: CreateIngestionKeyLimit
parameters:
- in: path
name: keyId
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GatewaytypesPostableIngestionKeyLimit'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/GatewaytypesGettableCreatedIngestionKeyLimit'
status:
type: string
type: object
description: Created
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create limit for the ingestion key
tags:
- gateway
/api/v2/gateway/ingestion_keys/limits/{limitId}:
delete:
deprecated: false
description: This endpoint deletes an ingestion key limit
operationId: DeleteIngestionKeyLimit
parameters:
- in: path
name: limitId
required: true
schema:
type: string
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete limit for the ingestion key
tags:
- gateway
patch:
deprecated: false
description: This endpoint updates an ingestion key limit
operationId: UpdateIngestionKeyLimit
parameters:
- in: path
name: limitId
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GatewaytypesUpdatableIngestionKeyLimit'
responses:
"204":
description: No Content
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update limit for the ingestion key
tags:
- gateway
/api/v2/gateway/ingestion_keys/search:
get:
deprecated: false
description: This endpoint returns the ingestion keys for a workspace
operationId: SearchIngestionKeys
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/GatewaytypesGettableIngestionKeys'
status:
type: string
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Search ingestion keys for workspace
tags:
- gateway
/api/v2/metric/alerts:
get:
deprecated: false
@@ -2736,12 +3091,25 @@ paths:
- sessions
components:
schemas:
AuthtypesAttributeMapping:
properties:
email:
type: string
groups:
type: string
name:
type: string
role:
type: string
type: object
AuthtypesAuthDomainConfig:
properties:
googleAuthConfig:
$ref: '#/components/schemas/AuthtypesGoogleConfig'
oidcConfig:
$ref: '#/components/schemas/AuthtypesOIDCConfig'
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
samlConfig:
$ref: '#/components/schemas/AuthtypesSamlConfig'
ssoEnabled:
@@ -2775,11 +3143,6 @@ components:
url:
type: string
type: object
AuthtypesClaimMapping:
properties:
email:
type: string
type: object
AuthtypesDeprecatedGettableLogin:
properties:
accessJwt:
@@ -2811,6 +3174,8 @@ components:
$ref: '#/components/schemas/AuthtypesOIDCConfig'
orgId:
type: string
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
samlConfig:
$ref: '#/components/schemas/AuthtypesSamlConfig'
ssoEnabled:
@@ -2834,17 +3199,33 @@ components:
type: object
AuthtypesGoogleConfig:
properties:
allowedGroups:
items:
type: string
type: array
clientId:
type: string
clientSecret:
type: string
domainToAdminEmail:
additionalProperties:
type: string
type: object
fetchGroups:
type: boolean
fetchTransitiveGroupMembership:
type: boolean
insecureSkipEmailVerified:
type: boolean
redirectURI:
type: string
serviceAccountJson:
type: string
type: object
AuthtypesOIDCConfig:
properties:
claimMapping:
$ref: '#/components/schemas/AuthtypesClaimMapping'
$ref: '#/components/schemas/AuthtypesAttributeMapping'
clientId:
type: string
clientSecret:
@@ -2895,8 +3276,22 @@ components:
refreshToken:
type: string
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
type: string
groupMappings:
additionalProperties:
type: string
nullable: true
type: object
useRoleAttribute:
type: boolean
type: object
AuthtypesSamlConfig:
properties:
attributeMapping:
$ref: '#/components/schemas/AuthtypesAttributeMapping'
insecureSkipAuthNRequestsSigned:
type: boolean
samlCert:
@@ -3011,6 +3406,160 @@ components:
nullable: true
type: object
type: object
GatewaytypesGettableCreatedIngestionKey:
properties:
id:
type: string
value:
type: string
type: object
GatewaytypesGettableCreatedIngestionKeyLimit:
properties:
id:
type: string
type: object
GatewaytypesGettableIngestionKeys:
properties:
_pagination:
$ref: '#/components/schemas/GatewaytypesPagination'
keys:
items:
$ref: '#/components/schemas/GatewaytypesIngestionKey'
nullable: true
type: array
type: object
GatewaytypesIngestionKey:
properties:
created_at:
format: date-time
type: string
expires_at:
format: date-time
type: string
id:
type: string
limits:
items:
$ref: '#/components/schemas/GatewaytypesLimit'
nullable: true
type: array
name:
type: string
tags:
items:
type: string
nullable: true
type: array
updated_at:
format: date-time
type: string
value:
type: string
workspace_id:
type: string
type: object
GatewaytypesLimit:
properties:
config:
$ref: '#/components/schemas/GatewaytypesLimitConfig'
created_at:
format: date-time
type: string
id:
type: string
key_id:
type: string
metric:
$ref: '#/components/schemas/GatewaytypesLimitMetric'
signal:
type: string
tags:
items:
type: string
nullable: true
type: array
updated_at:
format: date-time
type: string
type: object
GatewaytypesLimitConfig:
properties:
day:
$ref: '#/components/schemas/GatewaytypesLimitValue'
second:
$ref: '#/components/schemas/GatewaytypesLimitValue'
type: object
GatewaytypesLimitMetric:
properties:
day:
$ref: '#/components/schemas/GatewaytypesLimitMetricValue'
second:
$ref: '#/components/schemas/GatewaytypesLimitMetricValue'
type: object
GatewaytypesLimitMetricValue:
properties:
count:
format: int64
type: integer
size:
format: int64
type: integer
type: object
GatewaytypesLimitValue:
properties:
count:
format: int64
type: integer
size:
format: int64
type: integer
type: object
GatewaytypesPagination:
properties:
page:
type: integer
pages:
type: integer
per_page:
type: integer
total:
type: integer
type: object
GatewaytypesPostableIngestionKey:
properties:
expires_at:
format: date-time
type: string
name:
type: string
tags:
items:
type: string
nullable: true
type: array
type: object
GatewaytypesPostableIngestionKeyLimit:
properties:
config:
$ref: '#/components/schemas/GatewaytypesLimitConfig'
signal:
type: string
tags:
items:
type: string
nullable: true
type: array
type: object
GatewaytypesUpdatableIngestionKeyLimit:
properties:
config:
$ref: '#/components/schemas/GatewaytypesLimitConfig'
tags:
items:
type: string
nullable: true
type: array
type: object
MetricsexplorertypesMetricAlert:
properties:
alertId:

View File

@@ -2,6 +2,7 @@ package oidccallbackauthn
import (
"context"
"fmt"
"net/url"
"github.com/SigNoz/signoz/pkg/authn"
@@ -19,25 +20,27 @@ const (
redirectPath string = "/api/v1/complete/oidc"
)
var (
scopes []string = []string{"email", oidc.ScopeOpenID}
)
var defaultScopes []string = []string{"email", "profile", oidc.ScopeOpenID}
var _ authn.CallbackAuthN = (*AuthN)(nil)
type AuthN struct {
settings factory.ScopedProviderSettings
store authtypes.AuthNStore
licensing licensing.Licensing
httpClient *client.Client
}
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn")
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
if err != nil {
return nil, err
}
return &AuthN{
settings: settings,
store: store,
licensing: licensing,
httpClient: httpClient,
@@ -126,7 +129,40 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
}
}
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
name := ""
if nameClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Name; nameClaim != "" {
if n, ok := claims[nameClaim].(string); ok {
name = n
}
}
var groups []string
if groupsClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Groups; groupsClaim != "" {
if claimValue, exists := claims[groupsClaim]; exists {
switch g := claimValue.(type) {
case []any:
for _, group := range g {
if gs, ok := group.(string); ok {
groups = append(groups, gs)
}
}
case string:
// Some IDPs return a single group as a string instead of an array
groups = append(groups, g)
default:
a.settings.Logger().WarnContext(ctx, "oidc: unsupported groups type", "type", fmt.Sprintf("%T", claimValue))
}
}
}
role := ""
if roleClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Role; roleClaim != "" {
if r, ok := claims[roleClaim].(string); ok {
role = r
}
}
return authtypes.NewCallbackIdentity(name, email, authDomain.StorableAuthDomain().OrgID, state, groups, role), nil
}
func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {
@@ -145,6 +181,13 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
return nil, nil, err
}
scopes := make([]string, len(defaultScopes))
copy(scopes, defaultScopes)
if authDomain.AuthDomainConfig().RoleMapping != nil && len(authDomain.AuthDomainConfig().RoleMapping.GroupMappings) > 0 {
scopes = append(scopes, "groups")
}
return oidcProvider, &oauth2.Config{
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,

View File

@@ -96,7 +96,26 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "saml: invalid email").WithAdditional("The nameID assertion is used to retrieve the email address, please check your IDP configuration and try again.")
}
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
name := ""
if nameAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Name; nameAttribute != "" {
if val := assertionInfo.Values.Get(nameAttribute); val != "" {
name = val
}
}
var groups []string
if groupAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Groups; groupAttribute != "" {
groups = assertionInfo.Values.GetAll(groupAttribute)
}
role := ""
if roleAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Role; roleAttribute != "" {
if val := assertionInfo.Values.Get(roleAttribute); val != "" {
role = val
}
}
return authtypes.NewCallbackIdentity(name, email, authDomain.StorableAuthDomain().OrgID, state, groups, role), nil
}
func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDomain) *authtypes.AuthNProviderInfo {

View File

@@ -0,0 +1,282 @@
package httpgateway
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/gatewaytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/tidwall/gjson"
)
type Provider struct {
settings factory.ScopedProviderSettings
config gateway.Config
httpClient *client.Client
licensing licensing.Licensing
}
func NewProviderFactory(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, ps factory.ProviderSettings, c gateway.Config) (gateway.Gateway, error) {
return New(ctx, ps, c, licensing)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config gateway.Config, licensing licensing.Licensing) (gateway.Gateway, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/gateway/httpgateway")
httpClient, err := client.New(
settings.Logger(),
providerSettings.TracerProvider,
providerSettings.MeterProvider,
client.WithRequestResponseLog(true),
client.WithRetryCount(3),
)
if err != nil {
return nil, err
}
return &Provider{
settings: settings,
config: config,
httpClient: httpClient,
licensing: licensing,
}, nil
}
func (provider *Provider) GetIngestionKeys(ctx context.Context, orgID valuer.UUID, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error) {
qParams := url.Values{}
qParams.Add("page", strconv.Itoa(page))
qParams.Add("per_page", strconv.Itoa(perPage))
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys", qParams, nil)
if err != nil {
return nil, err
}
var ingestionKeys []gatewaytypes.IngestionKey
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKeys); err != nil {
return nil, err
}
var pagination gatewaytypes.Pagination
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "_pagination").String()), &pagination); err != nil {
return nil, err
}
return &gatewaytypes.GettableIngestionKeys{
Keys: ingestionKeys,
Pagination: pagination,
}, nil
}
func (provider *Provider) SearchIngestionKeysByName(ctx context.Context, orgID valuer.UUID, name string, page, perPage int) (*gatewaytypes.GettableIngestionKeys, error) {
qParams := url.Values{}
qParams.Add("name", name)
qParams.Add("page", strconv.Itoa(page))
qParams.Add("per_page", strconv.Itoa(perPage))
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/search", qParams, nil)
if err != nil {
return nil, err
}
var ingestionKeys []gatewaytypes.IngestionKey
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKeys); err != nil {
return nil, err
}
var pagination gatewaytypes.Pagination
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "_pagination").String()), &pagination); err != nil {
return nil, err
}
return &gatewaytypes.GettableIngestionKeys{
Keys: ingestionKeys,
Pagination: pagination,
}, nil
}
func (provider *Provider) CreateIngestionKey(ctx context.Context, orgID valuer.UUID, name string, tags []string, expiresAt time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error) {
requestBody := gatewaytypes.PostableIngestionKey{
Name: name,
Tags: tags,
ExpiresAt: expiresAt,
}
requestBodyBytes, err := json.Marshal(requestBody)
if err != nil {
return nil, err
}
responseBody, err := provider.do(ctx, orgID, http.MethodPost, "/v1/workspaces/me/keys", nil, requestBodyBytes)
if err != nil {
return nil, err
}
var createdKeyResponse gatewaytypes.GettableCreatedIngestionKey
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &createdKeyResponse); err != nil {
return nil, err
}
return &createdKeyResponse, nil
}
func (provider *Provider) UpdateIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string, name string, tags []string, expiresAt time.Time) error {
requestBody := gatewaytypes.PostableIngestionKey{
Name: name,
Tags: tags,
ExpiresAt: expiresAt,
}
requestBodyBytes, err := json.Marshal(requestBody)
if err != nil {
return err
}
_, err = provider.do(ctx, orgID, http.MethodPatch, "/v1/workspaces/me/keys/"+keyID, nil, requestBodyBytes)
if err != nil {
return err
}
return nil
}
func (provider *Provider) DeleteIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string) error {
_, err := provider.do(ctx, orgID, http.MethodDelete, "/v1/workspaces/me/keys/"+keyID, nil, nil)
if err != nil {
return err
}
return nil
}
func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, keyID string, signal string, limitConfig gatewaytypes.LimitConfig, tags []string) (*gatewaytypes.GettableCreatedIngestionKeyLimit, error) {
requestBody := gatewaytypes.PostableIngestionKeyLimit{
Signal: signal,
Config: limitConfig,
Tags: tags,
}
requestBodyBytes, err := json.Marshal(requestBody)
if err != nil {
return nil, err
}
responseBody, err := provider.do(ctx, orgID, http.MethodPost, "/v1/workspaces/me/keys/"+keyID+"/limits", nil, requestBodyBytes)
if err != nil {
return nil, err
}
var createdIngestionKeyLimitResponse gatewaytypes.GettableCreatedIngestionKeyLimit
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &createdIngestionKeyLimitResponse); err != nil {
return nil, err
}
return &createdIngestionKeyLimitResponse, nil
}
func (provider *Provider) UpdateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string, limitConfig gatewaytypes.LimitConfig, tags []string) error {
requestBody := gatewaytypes.UpdatableIngestionKeyLimit{
Config: limitConfig,
Tags: tags,
}
requestBodyBytes, err := json.Marshal(requestBody)
if err != nil {
return err
}
_, err = provider.do(ctx, orgID, http.MethodPatch, "/v1/workspaces/me/limits/"+limitID, nil, requestBodyBytes)
if err != nil {
return err
}
return nil
}
func (provider *Provider) DeleteIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string) error {
_, err := provider.do(ctx, orgID, http.MethodDelete, "/v1/workspaces/me/limits/"+limitID, nil, nil)
if err != nil {
return err
}
return nil
}
func (provider *Provider) do(ctx context.Context, orgID valuer.UUID, method string, path string, queryParams url.Values, body []byte) ([]byte, error) {
license, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "no valid license found").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
// build url
requestURL := provider.config.URL.JoinPath(path)
// add query params to the url
if queryParams != nil {
requestURL.RawQuery = queryParams.Encode()
}
// build request
request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), bytes.NewBuffer(body))
if err != nil {
return nil, err
}
// add headers needed to call gateway
request.Header.Set("Content-Type", "application/json")
request.Header.Set("X-Signoz-Cloud-Api-Key", license.Key)
request.Header.Set("X-Consumer-Username", "lid:00000000-0000-0000-0000-000000000000")
request.Header.Set("X-Consumer-Groups", "ns:default")
// execute request
response, err := provider.httpClient.Do(request)
if err != nil {
return nil, err
}
// read response
defer response.Body.Close()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
// only 2XX
if response.StatusCode/100 == 2 {
return responseBody, nil
}
errorMessage := gjson.GetBytes(responseBody, "error").String()
if errorMessage == "" {
errorMessage = "an unknown error occurred"
}
// return error for non 2XX
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
}
func (provider *Provider) errFromStatusCode(code int, errorMessage string) error {
switch code {
case http.StatusBadRequest:
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, errorMessage)
case http.StatusUnauthorized:
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, errorMessage)
case http.StatusForbidden:
return errors.New(errors.TypeForbidden, errors.CodeForbidden, errorMessage)
case http.StatusNotFound:
return errors.New(errors.TypeNotFound, errors.CodeNotFound, errorMessage)
case http.StatusConflict:
return errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, errorMessage)
}
return errors.New(errors.TypeInternal, errors.CodeInternal, errorMessage)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@@ -36,6 +37,7 @@ type APIHandlerOptions struct {
GatewayUrl string
// Querier Influx Interval
FluxInterval time.Duration
GlobalConfig global.Config
}
type APIHandler struct {

View File

@@ -76,7 +76,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
ingestionUrl, signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
signozApiUrl, apiErr := ah.getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key)
if apiErr != nil {
RespondError(w, basemodel.WrapApiError(
apiErr, "couldn't deduce ingestion url and signoz api url",
@@ -84,7 +84,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
result.IngestionUrl = ingestionUrl
result.IngestionUrl = ah.opts.GlobalConfig.IngestionURL.String()
result.SigNozAPIUrl = signozApiUrl
gatewayUrl := ah.opts.GatewayUrl
@@ -186,7 +186,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
}
func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
string, string, *basemodel.ApiError,
string, *basemodel.ApiError,
) {
// TODO: remove this struct from here
type deploymentResponse struct {
@@ -200,7 +200,7 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
respBytes, err := ah.Signoz.Zeus.GetDeployment(ctx, licenseKey)
if err != nil {
return "", "", basemodel.InternalError(fmt.Errorf(
return "", basemodel.InternalError(fmt.Errorf(
"couldn't query for deployment info: error: %w", err,
))
}
@@ -209,7 +209,7 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
err = json.Unmarshal(respBytes, resp)
if err != nil {
return "", "", basemodel.InternalError(fmt.Errorf(
return "", basemodel.InternalError(fmt.Errorf(
"couldn't unmarshal deployment info response: error: %w", err,
))
}
@@ -219,16 +219,14 @@ func (ah *APIHandler) getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licens
if len(regionDns) < 1 || len(deploymentName) < 1 {
// Fail early if actual response structure and expectation here ever diverge
return "", "", basemodel.InternalError(fmt.Errorf(
return "", basemodel.InternalError(fmt.Errorf(
"deployment info response not in expected shape. couldn't determine region dns and deployment name",
))
}
ingestionUrl := fmt.Sprintf("https://ingest.%s", regionDns)
signozApiUrl := fmt.Sprintf("https://%s.%s", deploymentName, regionDns)
return ingestionUrl, signozApiUrl, nil
return signozApiUrl, nil
}
type ingestionKey struct {

View File

@@ -172,6 +172,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
FluxInterval: config.Querier.FluxInterval,
Gateway: gatewayProxy,
GatewayUrl: config.Gateway.URL.String(),
GlobalConfig: config.Global,
}
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)

View File

@@ -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'],

View File

@@ -1,7 +1,10 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && yarn run commitlint --edit $1
# Convert $1 to absolute path before cd
commit_msg_file="$(realpath "$1")"
cd frontend && yarn run commitlint "$commit_msg_file"
branch="$(git rev-parse --abbrev-ref HEAD)"

View File

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

@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true
}

View File

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

View File

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

View 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

View 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

View 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);
}
}

View 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;

View 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);
}
}

View 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;

View File

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

View 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;

View File

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

View 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;

View File

@@ -58,6 +58,7 @@
flex-direction: column;
gap: 16px;
padding-left: 30px;
margin-bottom: 1rem;
li {
position: relative;

View File

@@ -12,7 +12,7 @@ import {
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/constants';
import dayjs from 'dayjs';
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';

View File

@@ -8,11 +8,11 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { RelativeDurationSuggestionOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
import {
LexicalContext,
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import dayjs from 'dayjs';
import { Clock, PenLine, TriangleAlertIcon } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';

View File

@@ -8,7 +8,7 @@ import {
CustomTimeType,
LexicalContext,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import dayjs, { Dayjs } from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';

View File

@@ -24,7 +24,7 @@ import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/c
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -5,7 +5,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';

View File

@@ -12,7 +12,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';

View File

@@ -1,16 +1,16 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -55,11 +55,11 @@ function LogDetailInner({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -365,10 +365,10 @@ function LogDetailInner({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -6,6 +6,7 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -108,6 +109,7 @@ type ListLogViewProps = {
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
};
function ListLogView({
@@ -118,6 +120,7 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -131,7 +134,6 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -255,7 +257,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
@@ -264,6 +266,7 @@ function ListLogView({
ListLogView.defaultProps = {
activeLog: null,
handleChangeSelectedView: undefined,
};
LogGeneralField.defaultProps = {

View File

@@ -39,6 +39,7 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -52,7 +53,6 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
@@ -224,13 +224,12 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { MouseEvent } from 'react';
import { IField } from 'types/api/logs/fields';
@@ -14,6 +15,7 @@ export interface RawLogViewProps {
fontSize: FontSize;
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
export interface RawLogContentProps {

View File

@@ -211,7 +211,10 @@ describe('VariableItem Integration Tests', () => {
await user.clear(textInput);
await user.type(textInput, 'new-text-value');
// Should call onValueUpdate after debounce
// Blur the input to trigger the value update
await user.tab();
// Should call onValueUpdate after blur
await waitFor(
() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(

View File

@@ -4,7 +4,7 @@ import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -33,6 +33,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addTraceOperator,
panelType,
initialDataSource,
handleRunQuery,
} = useQueryBuilder();
const containerRef = useRef(null);
@@ -157,10 +158,29 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>): void => {
const target = e.target as HTMLElement | null;
const tagName = target?.tagName || '';
const isInputElement =
['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName) ||
(target?.getAttribute('contenteditable') || '').toLowerCase() === 'true';
// Allow input elements in qb to run the query when Cmd/Ctrl + Enter is pressed
if (isInputElement && (e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
handleRunQuery();
}
},
[handleRunQuery],
);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
<div className="qb-content-container" onKeyDownCapture={handleKeyDown}>
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}

View File

@@ -11,7 +11,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { get, isEmpty } from 'lodash-es';
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
@@ -171,6 +171,9 @@ function QueryAddOns({
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
const initializedRef = useRef(false);
const prevAvailableKeysRef = useRef<Set<string> | null>(null);
const { handleChangeQueryData } = useQueryOperations({
index,
query,
@@ -213,23 +216,41 @@ function QueryAddOns({
}
setAddOns(filteredAddOns);
const activeAddOnKeys = new Set(
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
.filter(([, path]) => hasValue(get(query, path)))
.map(([key]) => key),
);
const availableAddOnKeys = new Set(filteredAddOns.map((a) => a.key));
const previousKeys = prevAvailableKeysRef.current;
const hasAvailabilityItemsChanged =
previousKeys !== null &&
(previousKeys.size !== availableAddOnKeys.size ||
[...availableAddOnKeys].some((key) => !previousKeys.has(key)));
prevAvailableKeysRef.current = availableAddOnKeys;
const availableAddOnKeys = new Set(filteredAddOns.map((addOn) => addOn.key));
// Filter and set selected views: add-ons that are both active and available
setSelectedViews(
filteredAddOns.filter(
(addOn) =>
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
if (!initializedRef.current || hasAvailabilityItemsChanged) {
initializedRef.current = true;
const activeAddOnKeys = new Set(
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
.filter(([, path]) => hasValue(get(query, path)))
.map(([key]) => key),
);
// Initial seeding from query values on mount
setSelectedViews(
filteredAddOns.filter(
(addOn) =>
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
),
);
return;
}
setSelectedViews((prev) =>
prev.filter((view) =>
filteredAddOns.some((addOn) => addOn.key === view.key),
),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [panelType, isListViewPanel, query]);
}, [panelType, isListViewPanel, query, showReduceTo]);
const handleOptionClick = (e: RadioChangeEvent): void => {
if (selectedViews.find((view) => view.key === e.target.value.key)) {

View File

@@ -1379,8 +1379,6 @@ function QuerySearch({
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(getCurrentExpression());
} else {
handleRunQuery();
}
return true;
},

View File

@@ -410,8 +410,6 @@ function TraceOperatorEditor({
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(value);
} else {
handleRunQuery();
}
return true;
},

View File

@@ -270,44 +270,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
await waitFor(() => expect(onRun).toHaveBeenCalled(), { timeout: 2000 });
});
it('calls handleRunQuery when Mod-Enter without onRun', async () => {
const mockedHandleRunQuery = handleRunQueryMock as jest.MockedFunction<
() => void
>;
mockedHandleRunQuery.mockClear();
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
await userEvent.click(editor);
await userEvent.type(editor, SAMPLE_VALUE_TYPING_COMPLETE);
// Use fireEvent for keyboard shortcuts as userEvent might not work well with CodeMirror
const modKey = navigator.platform.includes('Mac') ? 'metaKey' : 'ctrlKey';
fireEvent.keyDown(editor, {
key: 'Enter',
code: 'Enter',
[modKey]: true,
keyCode: 13,
});
await waitFor(() => expect(mockedHandleRunQuery).toHaveBeenCalled(), {
timeout: 2000,
});
});
it('initializes CodeMirror with expression from queryData.filter.expression on mount', async () => {
const testExpression =
"http.status_code >= 500 AND service.name = 'frontend'";

View File

@@ -3,14 +3,21 @@
import '@testing-library/jest-dom';
import { jest } from '@jest/globals';
import { fireEvent, waitFor } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { render, screen } from 'tests/test-utils';
import { Having, IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { render, screen, userEvent } from 'tests/test-utils';
import {
Having,
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { UseQueryOperations } from 'types/common/operations.types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import { QueryV2 } from '../QueryV2';
import { QueryBuilderV2 } from '../../QueryBuilderV2';
// Local mocks for domain-specific heavy child components
jest.mock(
@@ -36,16 +43,87 @@ const mockedUseQueryOperations = jest.mocked(
useQueryOperations,
) as jest.MockedFunction<UseQueryOperations>;
describe('QueryV2 - base render', () => {
describe('QueryBuilderV2 + QueryV2 - base render', () => {
let handleRunQueryMock: jest.MockedFunction<() => void>;
beforeEach(() => {
const mockCloneQuery = jest.fn() as jest.MockedFunction<
(type: string, q: IBuilderQuery) => void
>;
handleRunQueryMock = jest.fn() as jest.MockedFunction<() => void>;
const baseQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
aggregateOperator: '',
aggregations: [],
timeAggregation: '',
spaceAggregation: '',
temporality: '',
functions: [],
filter: undefined,
filters: { items: [], op: 'AND' },
groupBy: [],
expression: '',
disabled: false,
having: [] as Having[],
limit: 10,
stepInterval: null,
orderBy: [],
legend: 'A',
};
const currentQueryObj: Query = {
id: 'test',
unit: undefined,
queryType: EQueryType.CLICKHOUSE,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseQuery],
queryFormulas: [],
queryTraceOperator: [],
},
};
const updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'] = (
q,
) => q;
const updateQueriesData: QueryBuilderContextType['updateQueriesData'] = (q) =>
q;
mockedUseQueryBuilder.mockReturnValue(({
// Only fields used by QueryV2
currentQuery: currentQueryObj,
stagedQuery: null,
lastUsedQuery: null,
setLastUsedQuery: jest.fn(),
supersetQuery: currentQueryObj,
setSupersetQuery: jest.fn(),
initialDataSource: null,
panelType: PANEL_TYPES.TABLE,
isEnabledQuery: true,
handleSetQueryData: jest.fn(),
handleSetTraceOperatorData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeAllQueryBuilderEntities: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
removeTraceOperator: jest.fn(),
addTraceOperator: jest.fn(),
cloneQuery: mockCloneQuery,
panelType: null,
addNewQueryItem: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: handleRunQueryMock,
resetQuery: jest.fn(),
handleOnUnitsChange: jest.fn(),
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
} as unknown) as QueryBuilderContextType);
mockedUseQueryOperations.mockReturnValue({
@@ -71,40 +149,7 @@ describe('QueryV2 - base render', () => {
});
it('renders limit input when dataSource is logs', () => {
const baseQuery: IBuilderQuery = {
queryName: 'A',
dataSource: DataSource.LOGS,
aggregateOperator: '',
aggregations: [],
timeAggregation: '',
spaceAggregation: '',
temporality: '',
functions: [],
filter: undefined,
filters: { items: [], op: 'AND' },
groupBy: [],
expression: '',
disabled: false,
having: [] as Having[],
limit: 10,
stepInterval: null,
orderBy: [],
legend: 'A',
};
render(
<QueryV2
index={0}
isAvailableToDisable
query={baseQuery}
version="v4"
onSignalSourceChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
signalSourceChangeEnabled={false}
queriesCount={1}
showTraceOperator={false}
hasTraceOperator={false}
/>,
);
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
// Ensure the Limit add-on input is present and is of type number
const limitInput = screen.getByPlaceholderText(
@@ -115,4 +160,43 @@ describe('QueryV2 - base render', () => {
expect(limitInput).toHaveAttribute('name', 'limit');
expect(limitInput).toHaveAttribute('data-testid', 'input-Limit');
});
it('Cmd+Enter on an input triggers handleRunQuery via container handler', async () => {
render(<QueryBuilderV2 panelType={PANEL_TYPES.TABLE} version="v4" />);
const limitInput = screen.getByPlaceholderText('Enter limit');
fireEvent.keyDown(limitInput, {
key: 'Enter',
code: 'Enter',
metaKey: true,
});
expect(handleRunQueryMock).toHaveBeenCalled();
const legendInput = screen.getByPlaceholderText('Write legend format');
fireEvent.keyDown(legendInput, {
key: 'Enter',
code: 'Enter',
metaKey: true,
});
expect(handleRunQueryMock).toHaveBeenCalled();
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
// Wait for CodeMirror to initialize
await waitFor(() => {
const editor = document.querySelector(CM_EDITOR_SELECTOR);
expect(editor).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
await userEvent.click(editor);
fireEvent.keyDown(editor, {
key: 'Enter',
code: 'Enter',
metaKey: true,
});
expect(handleRunQueryMock).toHaveBeenCalled();
});
});

View File

@@ -53,4 +53,5 @@ export enum QueryParams {
variables = 'variables',
version = 'version',
showNewCreateAlertsPage = 'showNewCreateAlertsPage',
source = 'source',
}

View File

@@ -301,6 +301,7 @@ export const initialQueryState: QueryState = {
builder: initialQueryBuilderData,
clickhouse_sql: [initialClickHouseData],
promql: [initialQueryPromQLData],
unit: '',
};
const initialQueryWithType: Query = {

View File

@@ -7,7 +7,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import GetMinMax from 'lib/getMinMax';
import { ArrowDown, ArrowUp, X } from 'lucide-react';

View File

@@ -13,7 +13,7 @@ import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSea
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';

View File

@@ -9,7 +9,7 @@ import {
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useQueries } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {

View File

@@ -37,10 +37,7 @@ function ThresholdItem({
);
if (units.length === 0) {
component = (
<Tooltip
trigger="hover"
title="Please select a Y-axis unit for the query first"
>
<Tooltip trigger="hover" title="No compatible units available">
<Select
placeholder="Unit"
value={threshold.unit ? threshold.unit : null}

View File

@@ -47,9 +47,17 @@ export function getCategoryByOptionId(id: string): string | undefined {
}
export function getCategorySelectOptionByName(
name: string,
name: string | undefined,
): DefaultOptionType[] {
if (!name) {
return [];
}
const categories = getYAxisCategories(YAxisSource.ALERTS);
if (!categories.length) {
return [];
}
return (
categories
.find((category) => category.name === name)

View File

@@ -1,6 +1,12 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable sonarjs/no-duplicate-string */
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import {
IDashboardVariable,
TSortVariableValuesType,
@@ -639,4 +645,186 @@ describe('VariableItem Component', () => {
await expectCircularDependencyError();
});
});
describe('Textbox Variable Default Value Handling', () => {
test('saves textbox variable with defaultValue and selectedValue set to textboxValue', async () => {
const user = userEvent.setup();
const textboxVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Textbox Variable',
type: 'TEXTBOX',
textboxValue: 'my-default-value',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(textboxVariable);
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with defaultValue and selectedValue equal to textboxValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: 'my-default-value',
defaultValue: 'my-default-value',
selectedValue: 'my-default-value',
}),
expect.anything(),
);
});
test('saves textbox variable with empty values when textboxValue is empty', async () => {
const user = userEvent.setup();
const textboxVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Textbox Variable',
type: 'TEXTBOX',
textboxValue: '',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(textboxVariable);
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with empty defaultValue and selectedValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: '',
defaultValue: '',
selectedValue: '',
}),
expect.anything(),
);
});
test('updates textbox defaultValue and selectedValue when user changes textboxValue input', async () => {
const user = userEvent.setup();
const textboxVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Textbox Variable',
type: 'TEXTBOX',
textboxValue: 'initial-value',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(textboxVariable);
// Change the textbox value
const textboxInput = screen.getByPlaceholderText(
'Enter a default value (if any)...',
);
await user.clear(textboxInput);
await user.type(textboxInput, 'updated-value');
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with the updated defaultValue and selectedValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: 'updated-value',
defaultValue: 'updated-value',
selectedValue: 'updated-value',
}),
expect.anything(),
);
});
test('non-textbox variables use variableDefaultValue instead of textboxValue', async () => {
const user = userEvent.setup();
const queryVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Query Variable',
type: 'QUERY',
queryValue: 'SELECT * FROM test',
textboxValue: 'should-not-be-used',
defaultValue: 'query-default-value',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(queryVariable);
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with defaultValue not being textboxValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'QUERY',
defaultValue: 'query-default-value',
}),
expect.anything(),
);
// Verify that defaultValue is NOT the textboxValue
const savedVariable = onSave.mock.calls[0][1];
expect(savedVariable.defaultValue).not.toBe('should-not-be-used');
});
test('switching to textbox type sets defaultValue and selectedValue correctly on save', async () => {
const user = userEvent.setup();
// Start with a QUERY variable
const queryVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Variable',
type: 'QUERY',
queryValue: 'SELECT * FROM test',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(queryVariable);
// Switch to TEXTBOX type
const textboxButton = findButtonByText(TEXT.TEXTBOX);
expect(textboxButton).toBeInTheDocument();
if (textboxButton) {
await user.click(textboxButton);
}
// Enter a default value in the textbox input
const textboxInput = screen.getByPlaceholderText(
'Enter a default value (if any)...',
);
await user.type(textboxInput, 'new-textbox-default');
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with type TEXTBOX and correct defaultValue and selectedValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: 'new-textbox-default',
defaultValue: 'new-textbox-default',
selectedValue: 'new-textbox-default',
}),
expect.anything(),
);
});
});
});

View File

@@ -320,6 +320,10 @@ function VariableItem({
]);
const variableValue = useMemo(() => {
if (queryType === 'TEXTBOX') {
return variableTextboxValue;
}
if (variableMultiSelect) {
let value = variableData.selectedValue;
if (isEmpty(value)) {
@@ -352,6 +356,8 @@ function VariableItem({
variableData.selectedValue,
variableData.showALLOption,
variableDefaultValue,
variableTextboxValue,
queryType,
previewValues,
]);
@@ -367,13 +373,10 @@ function VariableItem({
multiSelect: variableMultiSelect,
showALLOption: queryType === 'DYNAMIC' ? true : variableShowALLOption,
sort: variableSortType,
...(queryType === 'TEXTBOX' && {
selectedValue: (variableData.selectedValue ||
variableTextboxValue) as never,
}),
...(queryType !== 'TEXTBOX' && {
defaultValue: variableDefaultValue as never,
}),
// the reason we need to do this is because defaultValues are treated differently in case of textbox type
// They are the exact same and not like the other types where defaultValue is a separate field
defaultValue:
queryType === 'TEXTBOX' ? variableTextboxValue : variableDefaultValue,
modificationUUID: generateUUID(),
id: variableData.id || generateUUID(),
order: variableData.order,

View File

@@ -25,6 +25,12 @@
}
}
&.focused {
.variable-value {
outline: 1px solid var(--bg-robin-400);
}
}
.variable-value {
display: flex;
min-width: 120px;
@@ -93,6 +99,12 @@
.lightMode {
.variable-item {
&.focused {
.variable-value {
border: 1px solid var(--bg-robin-400);
}
}
.variable-name {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -94,7 +94,7 @@ function DashboardVariableSelection(): JSX.Element | null {
cycleNodes,
});
}
}, [setVariablesToGetUpdated, variables, variablesTableData]);
}, [variables, variablesTableData]);
// this handles the case where the dependency order changes i.e. variable list updated via creation or deletion etc. and we need to refetch the variables
// also trigger when the global time changes

View File

@@ -80,10 +80,12 @@ describe('VariableItem', () => {
/>
</MockQueryClientProvider>,
);
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
expect(
screen.getByTestId('variable-textbox-test_variable'),
).toBeInTheDocument();
});
test('calls onChange event handler when Input value changes', async () => {
test('calls onValueUpdate when Input value changes and blurs', async () => {
render(
<MockQueryClientProvider>
<VariableItem
@@ -102,13 +104,19 @@ describe('VariableItem', () => {
</MockQueryClientProvider>,
);
const inputElement = screen.getByTestId('variable-textbox-test_variable');
// Change the value
act(() => {
const inputElement = screen.getByPlaceholderText('Enter value');
fireEvent.change(inputElement, { target: { value: 'newValue' } });
});
// Blur the input to trigger the update
act(() => {
fireEvent.blur(inputElement);
});
await waitFor(() => {
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'testVariable',
'test_variable',

View File

@@ -8,14 +8,14 @@ import './DashboardVariableSelection.styles.scss';
import { orange } from '@ant-design/colors';
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
import { Input, Popover, Tooltip, Typography } from 'antd';
import { Input, InputRef, Popover, Tooltip, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { debounce, isArray, isEmpty, isString } from 'lodash-es';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -71,6 +71,15 @@ function VariableItem({
string | string[] | undefined
>(undefined);
// Local state for textbox input to ensure smooth editing experience
const [textboxInputValue, setTextboxInputValue] = useState<string>(
(variableData.selectedValue?.toString() ||
variableData.defaultValue?.toString()) ??
'',
);
const [isTextboxFocused, setIsTextboxFocused] = useState<boolean>(false);
const textboxInputRef = useRef<InputRef>(null);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -371,7 +380,7 @@ function VariableItem({
}, [variableData.type, variableData.customValue]);
return (
<div className="variable-item">
<div className={`variable-item${isTextboxFocused ? ' focused' : ''}`}>
<Typography.Text className="variable-name" ellipsis>
${variableData.name}
{variableData.description && (
@@ -384,16 +393,40 @@ function VariableItem({
<div className="variable-value">
{variableData.type === 'TEXTBOX' ? (
<Input
ref={textboxInputRef}
placeholder="Enter value"
data-testid={`variable-textbox-${variableData.id}`}
bordered={false}
key={variableData.selectedValue?.toString()}
defaultValue={variableData.selectedValue?.toString()}
value={textboxInputValue}
title={textboxInputValue}
onChange={(e): void => {
debouncedHandleChange(e.target.value || '');
setTextboxInputValue(e.target.value);
}}
style={{
width:
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
onFocus={(): void => {
setIsTextboxFocused(true);
}}
onBlur={(e): void => {
setIsTextboxFocused(false);
const value = e.target.value.trim();
// If empty, reset to default value
if (!value && variableData.defaultValue) {
setTextboxInputValue(variableData.defaultValue.toString());
debouncedHandleChange(variableData.defaultValue.toString());
} else {
debouncedHandleChange(value);
}
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
const value = textboxInputValue.trim();
if (!value && variableData.defaultValue) {
setTextboxInputValue(variableData.defaultValue.toString());
debouncedHandleChange(variableData.defaultValue.toString());
} else {
debouncedHandleChange(value);
}
textboxInputRef.current?.blur();
}
}}
/>
) : (

View File

@@ -257,6 +257,15 @@ export const onUpdateVariableNode = (
): void => {
const visited = new Set<string>();
// If nodeToUpdate is not in topologicalOrder (e.g., CUSTOM variable),
// we still need to mark its children as needing updates
if (!topologicalOrder.includes(nodeToUpdate)) {
// Mark direct children of the node as visited so they get processed
(graph[nodeToUpdate] || []).forEach((child) => {
visited.add(child);
});
}
// Start processing from the node to update
topologicalOrder.forEach((node) => {
if (node === nodeToUpdate || visited.has(node)) {

View File

@@ -1,4 +1,4 @@
import { areArraysEqual } from './util';
import { areArraysEqual, onUpdateVariableNode, VariableGraph } from './util';
describe('areArraysEqual', () => {
it('should return true for equal arrays with same order', () => {
@@ -31,3 +31,121 @@ describe('areArraysEqual', () => {
expect(areArraysEqual(array1, array2)).toBe(true);
});
});
describe('onUpdateVariableNode', () => {
// Graph structure:
// deployment -> namespace -> service -> pod
// deployment has no parents, namespace depends on deployment, etc.
const graph: VariableGraph = {
deployment: ['namespace'],
namespace: ['service'],
service: ['pod'],
pod: [],
customVar: ['namespace'], // CUSTOM variable that affects namespace
};
const topologicalOrder = ['deployment', 'namespace', 'service', 'pod'];
it('should call callback for the node and all its descendants', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode('deployment', graph, topologicalOrder, callback);
expect(visited).toEqual(['deployment', 'namespace', 'service', 'pod']);
});
it('should call callback starting from a middle node', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode('namespace', graph, topologicalOrder, callback);
expect(visited).toEqual(['namespace', 'service', 'pod']);
});
it('should only call callback for the leaf node when updating leaf', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode('pod', graph, topologicalOrder, callback);
expect(visited).toEqual(['pod']);
});
it('should handle CUSTOM variable not in topologicalOrder by updating its children', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
// customVar is not in topologicalOrder but has namespace as a child
onUpdateVariableNode('customVar', graph, topologicalOrder, callback);
// Should process namespace and its descendants (service, pod)
expect(visited).toEqual(['namespace', 'service', 'pod']);
});
it('should handle node not in graph gracefully', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode('unknownNode', graph, topologicalOrder, callback);
// Should not call callback for any node since unknownNode has no children
expect(visited).toEqual([]);
});
it('should handle empty graph', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode('deployment', {}, topologicalOrder, callback);
// deployment is in topologicalOrder, so callback is called for it
expect(visited).toEqual(['deployment']);
});
it('should handle empty topologicalOrder', () => {
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode('deployment', graph, [], callback);
expect(visited).toEqual([]);
});
it('should handle CUSTOM variable with multiple children', () => {
const graphWithMultipleChildren: VariableGraph = {
...graph,
customMulti: ['namespace', 'service'], // CUSTOM variable affecting multiple nodes
};
const visited: string[] = [];
const callback = (node: string): void => {
visited.push(node);
};
onUpdateVariableNode(
'customMulti',
graphWithMultipleChildren,
topologicalOrder,
callback,
);
// Should process namespace, service, and pod (descendants)
expect(visited).toEqual(['namespace', 'service', 'pod']);
});
});

View File

@@ -15,11 +15,10 @@ import GridPanelSwitch from 'container/GridPanelSwitch';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import {
CustomTimeType,
Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config';
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -60,7 +59,7 @@ export interface ChartPreviewProps {
query: Query | null;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time | TimeV2 | CustomTimeType;
selectedInterval?: Time | CustomTimeType;
headline?: JSX.Element;
alertDef?: AlertDef;
userQueryKey?: string;

View File

@@ -4,3 +4,7 @@
gap: 8px;
align-items: center;
}
.rule-unit-selector {
width: 150px;
}

View File

@@ -15,8 +15,7 @@ import { DefaultOptionType } from 'antd/es/select';
import {
getCategoryByOptionId,
getCategorySelectOptionByName,
} from 'container/NewWidget/RightContainer/alertFomatCategories';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
} from 'container/CreateAlertV2/AlertCondition/utils';
import { useTranslation } from 'react-i18next';
import {
AlertDef,
@@ -43,10 +42,10 @@ function RuleOptions({
setAlertDef,
queryCategory,
queryOptions,
yAxisUnit,
}: RuleOptionsProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const { currentQuery } = useQueryBuilder();
const { ruleType } = alertDef;
@@ -365,11 +364,9 @@ function RuleOptions({
</InlineSelect>
);
const selectedCategory = getCategoryByOptionId(currentQuery?.unit || '');
const selectedCategory = getCategoryByOptionId(yAxisUnit);
const categorySelectOptions = getCategorySelectOptionByName(
selectedCategory?.name,
);
const categorySelectOptions = getCategorySelectOptionByName(selectedCategory);
const step3Label = alertDef.alertType === 'METRIC_BASED_ALERT' ? '3' : '2';
@@ -402,6 +399,7 @@ function RuleOptions({
<Form.Item noStyle>
<Select
className="rule-unit-selector"
getPopupContainer={popupContainer}
allowClear
showSearch
@@ -515,5 +513,6 @@ interface RuleOptionsProps {
setAlertDef: (a: AlertDef) => void;
queryCategory: EQueryType;
queryOptions: DefaultOptionType[];
yAxisUnit: string;
}
export default RuleOptions;

View File

@@ -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,
@@ -907,6 +914,7 @@ function FormAlertRules({
alertDef={alertDef}
setAlertDef={setAlertDef}
queryOptions={queryOptions}
yAxisUnit={yAxisUnit || ''}
/>
{renderBasicInfo()}

View File

@@ -1,5 +1,5 @@
import { SelectProps } from 'antd';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import getStep from 'lib/getStep';
import {

View File

@@ -3,7 +3,7 @@ import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
@@ -318,7 +318,9 @@ function GridCardGraph({
version={version}
threshold={threshold}
headerMenuList={menuList}
isFetchingResponse={queryResponse.isFetching}
isFetchingResponse={
queryResponse.isFetching || variablesToGetUpdated.length > 0
}
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}

View File

@@ -26,7 +26,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -21,7 +21,7 @@ import {
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -26,7 +26,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -16,7 +16,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { isArray } from 'lodash-es';

View File

@@ -4,7 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';

View File

@@ -7,7 +7,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';

View File

@@ -14,7 +14,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';

View File

@@ -2,7 +2,7 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { render, screen } from '@testing-library/react';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';

View File

@@ -15,7 +15,7 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';

View File

@@ -3,7 +3,7 @@
import { render, screen } from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { Time } from 'container/TopNav/DateTimeSelectionV2/config';
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';

View File

@@ -21,7 +21,7 @@ import {
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -23,7 +23,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -26,7 +26,7 @@ import NodeEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEv
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -27,7 +27,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -26,7 +26,7 @@ import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/util
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';

View File

@@ -9,7 +9,7 @@ import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import GetMinMax from 'lib/getMinMax';
import { X } from 'lucide-react';

View File

@@ -4,6 +4,7 @@ import { Switch, Typography } from 'antd';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
@@ -21,7 +22,13 @@ import { ILiveLogsLog } from '../LiveLogsList/types';
import LiveLogsListChart from '../LiveLogsListChart';
import { QueryHistoryState } from '../types';
function LiveLogsContainer(): JSX.Element {
interface LiveLogsContainerProps {
handleChangeSelectedView?: ChangeViewFunctionType;
}
function LiveLogsContainer({
handleChangeSelectedView,
}: LiveLogsContainerProps): JSX.Element {
const location = useLocation();
const [logs, setLogs] = useState<ILiveLogsLog[]>([]);
const { currentQuery, stagedQuery } = useQueryBuilder();
@@ -247,6 +254,7 @@ function LiveLogsContainer(): JSX.Element {
<LiveLogsList
logs={logs}
isLoading={initialLoading && logs.length === 0}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
</div>
@@ -256,4 +264,8 @@ function LiveLogsContainer(): JSX.Element {
);
}
LiveLogsContainer.defaultProps = {
handleChangeSelectedView: undefined,
};
export default LiveLogsContainer;

View File

@@ -25,7 +25,11 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { LiveLogsListProps } from './types';
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
function LiveLogsList({
logs,
isLoading,
handleChangeSelectedView,
}: LiveLogsListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { isConnectionLoading } = useEventSource();
@@ -36,7 +40,6 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -72,6 +75,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
@@ -85,10 +89,12 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
@@ -147,6 +153,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
appendTo: 'end',
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -170,12 +177,11 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</div>
);
}
export default memo(LiveLogsList);

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export interface ILiveLogsLog {
@@ -8,4 +9,5 @@ export interface ILiveLogsLog {
export type LiveLogsListProps = {
logs: ILiveLogsLog[];
isLoading: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};

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