Compare commits

...

75 Commits

Author SHA1 Message Date
nityanandagohain
38b1d92252 fix: revert domain fixes 2025-06-12 22:38:16 +05:30
primus-bot[bot]
17f48d656d chore(release): bump to v0.87.0 (#8222)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-06-11 12:06:17 +05:30
Srikanth Chekuri
2d6774da68 fix: add missing denominator for reset case (#8180) 2025-06-11 11:32:50 +05:30
Vibhu Pandey
62a9d7e602 docs(contributing): add endpoint docs (#8215)
* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs
2025-06-10 17:25:07 +00:00
Vikrant Gupta
3a2c7a7a68 fix(dashboard): create dashboard panic for id (#8214) 2025-06-10 21:31:56 +05:30
Sahil Khan
33e70d1f37 fix: traces back button issue (#8041)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2025-06-10 13:25:59 +00:00
Srikanth Chekuri
85f04e4bae chore: add querier HTTP API endpoint and bucket cache implementation (#8178)
* chore: update types
1. add partial bool to indicate if the value covers the partial interval
2. add optional unit if present (ex: duration_nano, metrics with units)
3. use pointers wherever necessary
4. add format options for request and remove redundant name in query envelope

* chore: fix some gaps
1. make the range as [start, end)
2. provide the logs statement builder with the body column
3. skip the body filter on resource filter statement builder
4. remove unnecessary agg expr rewriter in metrics
5. add ability to skip full text in where clause visitor

* chore: add API endpoint for new query range

* chore: add bucket cache implementation

* chore: add fingerprinting impl and add bucket cache to querier

* chore: add provider factory
2025-06-10 12:56:28 +00:00
Shaheer Kochai
53f9e7d811 chore: trace funnels bugfixes/improvements (#8114)
* fix: refetch funnel steps overview on clicking refresh

* chore: temporarily hide latency pointer from funnel steps

* chore: remove the existing filters of a step on clicking replace button

* fix(useLocalStorage): stabilize initialValue handling to prevent unnecessary re-renders

* chore: remove p99_latency references from funnel metrics

* fix(useFunnelMetrics): ensure latency type defaults to P99 when undefined
2025-06-10 12:45:25 +00:00
Vibhu Pandey
ad46e22561 docs(contributing): add provider docs (#8193) 2025-06-10 05:39:14 +00:00
Yunus M
e79195ccf1 fix: handle alert list updates (#8109) 2025-06-10 10:56:45 +05:30
Amlan Kumar Nandy
f77bb888a8 chore: add analytics for metrics explorer (#8108) 2025-06-10 04:42:49 +00:00
Amlan Kumar Nandy
baa15baea9 chore: metrics explorer summary view fixes (#8126) 2025-06-10 04:18:12 +00:00
Vibhu Pandey
316e6821f1 feat(statsreporter): report stats on stop (#8187) 2025-06-10 07:55:32 +05:30
Vibhu Pandey
a1fa2769e4 feat(statsreporter): build a statsreporter service (#8177)
- build a new statsreporter service
2025-06-09 16:43:29 +05:30
Vikrant Gupta
decb660992 chore(sqlmigration): drop the rule history and data migrations table (#8181) 2025-06-09 15:46:22 +05:30
Amlan Kumar Nandy
0acbcf8322 chore: remove critters-webpack-plugin (#8172) 2025-06-07 16:21:06 +07:00
dependabot[bot]
11eabdc2ac chore(deps): bump webpack-dev-server from 4.15.2 to 5.2.1 in /frontend (#8160)
* chore(deps): bump webpack-dev-server from 4.15.2 to 5.2.1 in /frontend

Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat: upgraded webpack-cli for compatibility fix for webpack-dev-server upgrade

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <sagar@signoz.io>
2025-06-07 07:24:18 +00:00
Vibhu Pandey
eb94554f5a feat(preference): add support for objects and arrays (#8142)
* refactor(preference): better readability

* refactor: better readability

* refactor: better readability

* fix: change frontend contract

* refactor: change frontend

* refactor: change frontend

* refactor: change frontend

* refactor: change frontend

* chore: fix tsc

* chore: fix tsc

* chore: fix tsc

* chore: fix tsc
2025-06-06 22:38:28 +05:30
Piyush Singariya
e8280dbea4 feat: Adding ContainerInsights in ECS Integrations (AWS) (#8122)
* fix: adding ECS ContainerInsights

* chore: dashboard added

* chore: format integrations.json

* feat(7294): added _dot metrics for aws ecs

---------

Co-authored-by: aniket <aniket@signoz.io>
2025-06-06 09:27:35 +00:00
Nityananda Gohain
44ea237039 fix: remove whitespace from sso cert (#8141)
* fix: remove whitespace from sso cert

* fix: use trimspace instead

* fix: use replaceall
2025-06-06 09:03:46 +00:00
Srikanth Chekuri
72b0214d1d chore: add range query impl for promql (#8130) 2025-06-05 19:18:44 +00:00
Srikanth Chekuri
386a215324 chore: metric statement builder (#8104) 2025-06-06 00:38:48 +05:30
Vibhu Pandey
ba0ba4bbc9 build(go): upgrade purego to v0.8.4 (#8159) 2025-06-05 12:31:49 +00:00
SagarRajput-7
d60c9ab36b feat: handle unkown metric in panel query (#8083)
* feat: handle unkown metric in panel query

* feat: added handling with type empty and key present or not

* feat: added test cases

* feat: added comment to better explain the logic

* feat: fixed operator list for unkown metric

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-06-05 11:23:25 +00:00
Piyush Singariya
90770b90bd feat: Introducing EKS integration (AWS) (#8021)
* feat: introducing EKS integration (AWS)

* fix: update metrics and enable logs collection

* feat: eks Overview dashboard ready

* feat: containerinsights incoming

* chore: dashboard name update

* feat(7294): added _dot metrics for aws ecs

* feat(7274): added dot metrics for overview metrics in eks

* Update pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/overview_dot.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/containerinsights_dot.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: aniket <aniket@signoz.io>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-05 15:35:39 +05:30
Vikrant Gupta
a19874c1dd fix(dashboard): dashboards/alerts info telemetry fix (#8161) 2025-06-05 13:47:25 +05:30
Piyush Singariya
65ff460d63 fix: Enhance filter support for Pipeline Simulation (#8134)
* feat: enhance filter support for JSON log body

* test: added tests for exists and not exists

* test: remove the value
2025-06-05 05:05:39 +00:00
primus-bot[bot]
b9d542a294 chore(release): bump to v0.86.2 (#8154) 2025-06-04 14:53:32 +00:00
aniketio-ctrl
e75e5bdbdb feat(7294): added flag columns in query (#8153)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-06-04 20:10:52 +05:30
Srikanth Chekuri
0d03203977 chore: add formula evaluator (#8112) 2025-06-04 13:40:42 +00:00
primus-bot[bot]
28f6f42ac4 chore(release): bump to v0.86.1 (#8152)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-06-04 13:16:42 +05:30
Vibhu Pandey
92f8e4d5b9 fix(alertmanager): fix legacy alertmanager injection (#8151) 2025-06-04 07:29:40 +00:00
primus-bot[bot]
037eea5262 chore(release): bump to v0.86.0 (#8150)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-06-04 12:23:01 +05:30
SagarRajput-7
cd4df6280f feat: fixed multiple sentry error around dashboards (#8148) 2025-06-04 13:20:51 +07:00
Vikrant Gupta
ad2d4ed56c chore(dashboard): mismatch in dashboard lock rbac (#8137) 2025-06-03 20:06:38 +05:30
aniketio-ctrl
7955497a8d Feat/7294 (#8139)
* feat(7294): updated dashboard uri for cloud integrations
2025-06-03 19:43:42 +05:30
aniketio-ctrl
6ed30318bd feat(7294): updated dashboard uri for cloud integrations (#8135) 2025-06-03 17:48:31 +05:30
Vikrant Gupta
c32dd9f17e chore(feature): drop the feature status table (#8124)
* chore(feature): drop the feature set table

* chore(feature): cleanup the types and remove unused flags

* chore(feature): some more cleanup

* chore(feature): add codeowners file

* chore(feature): init to basic plan for failed validations

* chore(feature): cleanup

* chore(feature): pkg handler cleanup

* chore(feature): pkg handler cleanup

* chore(feature): address review comments

* chore(feature): address review comments

* chore(feature): address review comments

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-06-03 17:05:42 +05:30
Shaheer Kochai
c58cf67eb0 refactor: update funnel description endpoint from POST /save to PUT /{funnel_id} (#8080)
* refactor: update funnel description endpoint from POST /save to PUT /{funnel_id}

* feat: add timestamp to funnel description payload and update mutation type

---------

Co-authored-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 10:59:54 +00:00
Nageshbansal
440c3d8386 fix: Broken Docker Downloads Badge (#7954)
* Updated the Badge for Docker Downloads to use signoz/signoz repo
2025-06-03 16:01:28 +05:30
Aditya Singh
d683b94344 [Fix #8102] Logs Issues with context view (#8111)
* fix: add active log id to charts query

* refactor: remove comment

* fix: remove active log id on filter change

* test: update test for log explorer

* test: update test for log explorer

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-06-03 09:57:39 +00:00
Srikanth Chekuri
6a629623bc chore: port functions, reduce to, series limit support (#8105) 2025-06-03 11:37:47 +05:30
Srikanth Chekuri
982688ccc9 chore: add field mapper and condition builder for ts v4 (#8100) 2025-06-02 19:43:48 +00:00
aniketio-ctrl
74bbb26033 fix(metrics): exclude NoRecordedValue data points from aggregation (#7674) 2025-06-03 00:40:05 +05:30
Vikrant Gupta
3bb9e05681 chore(dashboard): make dashboard schema production ready (#8092)
* chore(dashboard): intial commit

* chore(dashboard): bring all the code in module

* chore(dashboard): remove lock unlock from ee codebase

* chore(dashboard): go deps

* chore(dashboard): fix lint

* chore(dashboard): implement the store

* chore(dashboard): add migration

* chore(dashboard): fix lint

* chore(dashboard): api and frontend changes

* chore(dashboard): frontend changes for new dashboards

* chore(dashboard): fix test cases

* chore(dashboard): add lock unlock APIs

* chore(dashboard): add lock unlock APIs

* chore(dashboard): move integrations controller out from module

* chore(dashboard): move integrations controller out from module

* chore(dashboard): move integrations controller out from module

* chore(dashboard): rename migration file

* chore(dashboard): surface errors for lock/unlock dashboard

* chore(dashboard): some testing cleanups

* chore(dashboard): fix postgres migrations

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-06-02 22:41:38 +05:30
aniketio-ctrl
61b2f8cb31 fix(8082): removed unnecessary log lines (#8123) 2025-06-02 13:09:43 +00:00
Vikrant Gupta
9d397d0867 fix(license): fixes for license service (#8121)
* fix(license): fixes for license service

* fix(license): fixes for license service

* fix(license): add code comments
2025-06-02 17:09:19 +05:30
aniketio-ctrl
5fb4206a99 Feat/7294: Updated Dashboards for integrations (#8113) 2025-06-02 15:17:53 +05:30
Aditya Singh
dd11ba9f48 fix: remove create dashboard call before navigate (#8029)
* fix: remove create dashboard call before navigate

* feat: minor refactor

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-06-02 08:02:09 +00:00
Shivanshu Raj Shrivastava
f9cb9f10be feat: adds a part of trace funnel feature (APIs, module, handler, store, migrations) implementation (#7763)
* feat: adds server and handler changes

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add tracefunnel module and handler

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add required types for tracefunnels

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: db operations, module and handler implementation

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add db migrations

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: add utility functions

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add utility function tests

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add handler tests

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add trace funnel module tests

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: refactor handler and utils

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: add funnel validation while processing funnel steps

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add more tests to utils

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: fix package naming

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: fix naming convention

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: update normalize funnel steps

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: added some improvements

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: optimize funnel creation by combining insert and update operations

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: fix error handling

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: trace funnel state management

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: updated unit tests and mocks

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: review comments

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: minor fixes

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: update funnel migration number

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: review comments and some changes

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: update modules

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 07:00:49 +00:00
Aditya Singh
b6180f6957 fix: fix html escape and json string parsing in qb (#8039)
Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-06-02 06:42:02 +00:00
aniketio-ctrl
51d3ca16f7 fix(metric-explorer): case sensitivity in contains (#8103) 2025-06-02 04:35:32 +00:00
Vibhu Pandey
91cbd17275 feat(sharder): add simple and noop sharder (#8107) 2025-05-31 16:04:13 +05:30
aniketio-ctrl
68effaf232 chore: support for non-normalized metrics behind a feature flag (#7919)
feat(7294-services): added dot metrics boolean for services tab
2025-05-30 10:27:29 +00:00
Aditya Singh
c08d1bccaf FIX: Pipelines edit filter return empty filter (#8055)
* fix: fix pipeline add and edit form flow

* test: update test cases

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2025-05-30 15:47:26 +05:30
Amlan Kumar Nandy
1d77780c70 feat: add views tab to metrics explorer (#8091) 2025-05-30 05:39:24 +00:00
primus-bot[bot]
80ded899c7 chore(release): bump to v0.85.3 (#8099)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-05-29 17:45:38 +05:30
Vikrant Gupta
4733af974e fix(license): return the active license even in case of suspended status (#8097)
* fix(license): return the active license even in case of suspended status

* fix(license): suspended check for side nav

* fix(license): suspended check for side nav

* fix(license): suspended check for side nav

* fix(license): suspended check for side nav
2025-05-29 12:05:27 +00:00
Amlan Kumar Nandy
1ab6c7177f chore: infra monitoring fixes (#8066) 2025-05-29 10:53:47 +07:00
primus-bot[bot]
c3123a4fa4 chore(release): bump to v0.85.2 (#8089)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-05-28 16:42:27 +00:00
SagarRajput-7
5a602bbeb7 fix: added safety checks for query data (#8088) 2025-05-28 21:41:24 +05:30
SagarRajput-7
f487f088bd Revert "feat: improved the alert rules list search functionality" (#8085)
* Revert "feat: improved the alert rules list search functionality (#8075)"

This reverts commit bec52c3d3e.

* feat: added search capability for labels

* feat: added test cases
2025-05-28 21:24:26 +05:30
Vikrant Gupta
1cb01e8dd2 fix(saml): do not fetch the claims and use orgID from domain (#8086)
* fix(saml): do not fetch the claims and use orgID from domain

* fix(saml): do not fetch the claims and use orgID from domain
2025-05-28 18:21:35 +05:30
primus-bot[bot]
595a500be4 chore(release): bump to v0.85.0 (#8078)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-05-28 12:17:24 +05:30
SagarRajput-7
bec52c3d3e feat: improved the alert rules list search functionality (#8075)
* feat: improved the alert rules list search functionality

* feat: improvements and tooltip added for more info

* feat: style improvement

* feat: style improvement

* feat: style improvement
2025-05-28 05:23:42 +00:00
Srikanth Chekuri
0a6a7ba729 chore: show migration info to all cloud regions (#8077) 2025-05-28 04:48:42 +00:00
Vishal Sharma
3d758d4358 feat: init pylon and deprecate intercom (#8059) 2025-05-28 07:11:11 +05:30
Vishal Sharma
9c8435119d feat: add appcues and remove customerio (#8045) 2025-05-27 19:49:55 +00:00
Ekansh Gupta
d732f8ba42 fix: updated the service name in exceptions filter (#8069)
* fix: updated the service name in exceptions filter

* fix: updated the service name in exceptions filter

* fix: updated the service name in exceptions filter
2025-05-27 17:42:08 +00:00
Vibhu Pandey
83b8eaf623 feat(pylon|appcues): add pylon and appcues (#8073) 2025-05-27 17:32:45 +00:00
Vikrant Gupta
ae7364f098 fix(login): fixed the interceptor to handle multiple failures (#8071)
* fix(login): fixed the interceptor to handle multiple failures

* fix(login): fixed the interceptor to handle multiple failures
2025-05-27 15:47:33 +00:00
Srikanth Chekuri
0ec1be1ddf chore: add querier base implementation (#8028) 2025-05-27 20:54:48 +05:30
Yunus M
93de4681a9 feat: oss - sso and api keys (#8068)
* feat: oss - sso and api keys

* feat: show to community and community enterprise

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2025-05-27 20:29:09 +05:30
Aditya Singh
69e94cbd38 Custom Quick FIlters: Integration across other tabs (#8001)
* chore: added filters init

* chore: handle save and discard

* chore: search and api intergrations

* feat: search on filters

* feat: style fix

* feat: style fix

* feat: signal to data source config

* feat: search styles

* feat: update drawer slide style

* chore: quick filters - added filters init (#7867)

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

* feat: no results state

* fix: minor fix

* feat: qf setting ui

* feat: add skeleton to dynamic qf

* fix: minor fix

* feat: announcement tooltip added

* feat: announcement tooltip added refactor

* feat: announcement tooltip styles

* feat: announcement tooltip integration

* fix: number vals in filter list

* feat: announcement tooltip show logic added

* feat: light mode styles

* feat: remove unwanted styles

* feat: remove filter disable when one filter added

* style: minor style

* fix: minor refactor

* test: added test cases

* feat: integrate custom quick filters in logs

* Custom quick filter: Other Filters | Search integration (#7939)

* chore: added filters init

* chore: handle save and discard

* chore: search and api intergrations

* feat: search on filters

* feat: style fix

* feat: style fix

* feat: signal to data source config

* feat: search styles

* feat: update drawer slide style

* feat: no results state

* fix: minor fix

* Custom Quick FIlters: UI fixes and Announcement Tooltip (#7950)

* feat: qf setting ui

* feat: add skeleton to dynamic qf

* fix: minor fix

* feat: announcement tooltip added

* feat: announcement tooltip added refactor

* feat: announcement tooltip styles

* feat: announcement tooltip integration

* fix: number vals in filter list

* feat: announcement tooltip show logic added

* feat: light mode styles

* feat: remove unwanted styles

* feat: remove filter disable when one filter added

* style: minor style

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

* feat: code refactor

* feat: debounce search

* feat: refactor

* feat: exceptions integrate

* feat: api monitoring qf integrate

* feat: handle query name show

* Custom quick filters: Tests and pr review comments (#7967)

* chore: added filters init

* chore: handle save and discard

* chore: search and api intergrations

* feat: search on filters

* feat: style fix

* feat: style fix

* feat: signal to data source config

* feat: search styles

* feat: update drawer slide style

* feat: no results state

* fix: minor fix

* feat: qf setting ui

* feat: add skeleton to dynamic qf

* fix: minor fix

* feat: announcement tooltip added

* feat: announcement tooltip added refactor

* feat: announcement tooltip styles

* feat: announcement tooltip integration

* fix: number vals in filter list

* feat: announcement tooltip show logic added

* feat: light mode styles

* feat: remove unwanted styles

* feat: remove filter disable when one filter added

* style: minor style

* fix: minor refactor

* test: added test cases

* feat: integrate custom quick filters in logs

* feat: code refactor

* feat: debounce search

* feat: refactor

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>

* feat: integrate traces data source to settings

* feat: duration nano traces filter in qf

* fix: allow only admins to change qf  settings

* feat: has error handling

* feat: fix existing tests

* feat: update test cases

* feat: update test cases

* feat: minor refactor

* feat: minor refactor

* feat: log quick filter settings changes

* feat: log quick filter settings changes

* feat: log quick filter settings changes

---------

Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
2025-05-27 20:04:57 +05:30
Srikanth Chekuri
62810428d8 chore: add logs statement builder base (#8024) 2025-05-27 13:51:38 +00:00
605 changed files with 96418 additions and 19830 deletions

View File

@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.111.41
image: signoz/signoz-schema-migrator:v0.111.42
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.111.41
image: signoz/signoz-schema-migrator:v0.111.42
container_name: schema-migrator-async
command:
- async

2
.github/CODEOWNERS vendored
View File

@@ -11,5 +11,5 @@
/pkg/errors/ @grandwizard28
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
/pkg/sqlmigration/ @vikrantgupta25
.golangci.yml @grandwizard28
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25

View File

@@ -74,7 +74,8 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.variant=community
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch

View File

@@ -67,9 +67,8 @@ jobs:
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:
@@ -109,7 +108,8 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

@@ -66,7 +66,8 @@ jobs:
echo 'CI=1' > frontend/.env
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'USERPILOT_KEY="${{ secrets.NP_USERPILOT_KEY }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:
@@ -106,7 +107,8 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

@@ -33,9 +33,8 @@ jobs:
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> .env
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> .env
echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> .env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
- name: build-frontend
run: make js-build
- name: upload-frontend-artifact

1
.gitignore vendored
View File

@@ -66,6 +66,7 @@ e2e/.auth
# go
vendor/
**/main/**
__debug_bin**
# git-town
.git-branches.toml

View File

@@ -8,7 +8,7 @@
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/signoz.svg?label=Docker%20Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>

View File

@@ -165,12 +165,6 @@ alertmanager:
# Retention of the notification logs.
retention: 120h
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
##################### Emailing #####################
emailing:
# Whether to enable emailing.
@@ -207,3 +201,26 @@ emailing:
key_file_path:
# The path to the certificate file.
cert_file_path:
##################### Sharder (experimental) #####################
sharder:
# Specifies the sharder provider to use.
provider: noop
single:
# The org id to which this instance belongs to.
org_id: org_id
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
segment:
# The key to use for segment.
key: ""
##################### StatsReporter #####################
statsreporter:
# Whether to enable stats reporter. This is used to provide valuable insights to the SigNoz team. It does not collect any sensitive/PII data.
enabled: true
# The interval at which the stats are collected.
interval: 6h

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.84.1
image: signoz/signoz:v0.87.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -206,7 +206,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.111.41
image: signoz/signoz-otel-collector:v0.111.42
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -230,7 +230,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.111.41
image: signoz/signoz-schema-migrator:v0.111.42
deploy:
restart_policy:
condition: on-failure

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.84.1
image: signoz/signoz:v0.87.0
command:
- --config=/root/config/prometheus.yml
ports:
@@ -141,7 +141,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.111.41
image: signoz/signoz-otel-collector:v0.111.42
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -165,7 +165,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.111.41
image: signoz/signoz-schema-migrator:v0.111.42
deploy:
restart_policy:
condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.84.1}
image: signoz/signoz:${VERSION:-v0.87.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -210,7 +210,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.41}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -236,7 +236,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
container_name: schema-migrator-sync
command:
- sync
@@ -247,7 +247,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.84.1}
image: signoz/signoz:${VERSION:-v0.87.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -142,7 +142,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.41}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.42}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -164,7 +164,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
container_name: schema-migrator-sync
command:
- sync
@@ -176,7 +176,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.41}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.42}
container_name: schema-migrator-async
command:
- async

View File

@@ -0,0 +1,51 @@
# Endpoint
This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.
## How do we design an endpoint?
### Understand the core entities and their relationships
Start with understanding the core entities and their relationships. For example:
- **Organization**: an organization can have multiple users
### Structure Endpoints RESTfully
Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, **pluralized resource names** and versioning. For example:
- `POST /v1/organizations` — Create an organization
- `GET /v1/organizations/:id` — Get an organization by id
- `DELETE /v1/organizations/:id` — Delete an organization by id
- `PUT /v1/organizations/:id` — Update an organization by id
- `GET /v1/organizations/:id/users` — Get all users in an organization
- `GET /v1/organizations/me/users` — Get all users in my organization
Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the `organizations` directory. To find a user in an organization, you would navigate to the `organizations` directory and then to the `id` directory.
```bash
v1/
├── organizations/
│ └── 123/
│ └── users/
```
`me` endpoints are special. They are used to determine the actual id via some auth/external mechanism. For `me` endpoints, think of the `me` directory being symlinked to your organization directory. For example, if you are a part of the organization `123`, the `me` directory will be symlinked to `/v1/organizations/123`:
```bash
v1/
├── organizations/
│ └── me/ -> symlink to /v1/organizations/123
│ └── users/
│ └── 123/
│ └── users/
```
> 💡 **Note**: There are various ways to structure endpoints. Some prefer to use singular resource names instead of `me`. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.
## What should I remember?
- Use clear, **plural resource names**
- Use `me` endpoints for determining the actual id via some auth mechanism
> 💡 **Note**: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.

View File

@@ -0,0 +1,106 @@
# Provider
SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.
> 💡 **Note**: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.
## How to create a new provider?
To create a new provider, create a directory in the `pkg/` directory named after your provider. The provider package consists of four key components:
- **Interface** (`pkg/<name>/<name>.go`): Defines the provider's interface. Other packages should import this interface to use the provider.
- **Config** (`pkg/<name>/config.go`): Contains provider configuration, implementing the `factory.Config` interface from [factory/config.go](/pkg/factory/config.go).
- **Implementation** (`pkg/<name>/<implname><name>/provider.go`): Contains the provider implementation, including a `NewProvider` function that returns a `factory.Provider` interface from [factory/provider.go](/pkg/factory/provider.go).
- **Mock** (`pkg/<name>/<name>test.go`): Provides mocks for the provider, typically used by dependent packages for unit testing.
For example, the [prometheus](/pkg/prometheus) provider delivers a prometheus engine to the application:
- `pkg/prometheus/prometheus.go` - Interface definition
- `pkg/prometheus/config.go` - Configuration
- `pkg/prometheus/clickhouseprometheus/provider.go` - Clickhouse-powered implementation
- `pkg/prometheus/prometheustest/provider.go` - Mock implementation
## How to wire it up?
The `pkg/signoz` package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.
> 💡 **Note**: Coming from a Java background? Providers are similar to Spring beans.
Wiring up a provider involves three steps:
1. Wiring up the configuration
Add your config from `pkg/<name>/config.go` to the `pkg/signoz/config.Config` struct and in new factories:
```go
type Config struct {
...
MyProvider myprovider.Config `mapstructure:"myprovider"`
...
}
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
...
configFactories := []factory.ConfigFactory{
myprovider.NewConfigFactory(),
}
...
}
```
2. Wiring up the provider
Add available provider implementations in `pkg/signoz/provider.go`:
```go
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
return factory.MustNewNamedMap(
myproviderone.NewFactory(),
myprovidertwo.NewFactory(),
)
}
```
3. Instantiate the provider by adding it to the `SigNoz` struct in `pkg/signoz/signoz.go`:
```go
type SigNoz struct {
...
MyProvider myprovider.MyProvider
...
}
func New(...) (*SigNoz, error) {
...
myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
if err != nil {
return nil, err
}
...
}
```
## How to use it?
To use a provider, import its interface. For example, to use the prometheus provider, import `pkg/prometheus/prometheus.go`:
```go
import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"
func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
...
prometheus.DoSomething()
...
}
```
## Why do we need this?
Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:
- Configuration is **defined with each provider and centralized in one place**, making it easier to understand and manage through various methods (environment variables, config files, etc.)
- Provider mocking is **straightforward for unit testing**, with a consistent pattern for locating mocks
- **Multiple implementations** of the same provider are **supported**, as demonstrated by our sqlstore provider
## What should I remember?
- Use the provider pattern wherever applicable.
- Always create a provider **irrespective of the number of implementations**. This makes it easier to add new implementations in the future.

View File

@@ -9,8 +9,8 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -18,23 +18,31 @@ import (
)
type provider struct {
store licensetypes.Store
zeus zeus.Zeus
config licensing.Config
settings factory.ScopedProviderSettings
stopChan chan struct{}
store licensetypes.Store
zeus zeus.Zeus
config licensing.Config
settings factory.ScopedProviderSettings
orgGetter organization.Getter
stopChan chan struct{}
}
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config, store, zeus)
return New(ctx, providerSettings, config, store, zeus, orgGetter)
})
}
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus) (licensing.Licensing, error) {
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore)
return &provider{store: licensestore, zeus: zeus, config: config, settings: settings, stopChan: make(chan struct{})}, nil
return &provider{
store: licensestore,
zeus: zeus,
config: config,
settings: settings,
orgGetter: orgGetter,
stopChan: make(chan struct{}),
}, nil
}
func (provider *provider) Start(ctx context.Context) error {
@@ -66,20 +74,13 @@ func (provider *provider) Stop(ctx context.Context) error {
}
func (provider *provider) Validate(ctx context.Context) error {
organizations, err := provider.store.ListOrganizations(ctx)
organizations, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
return err
}
for _, organizationID := range organizations {
err := provider.Refresh(ctx, organizationID)
if err != nil {
return err
}
}
if len(organizations) == 0 {
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
for _, organization := range organizations {
err := provider.Refresh(ctx, organization.ID)
if err != nil {
return err
}
@@ -105,11 +106,6 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
return err
}
err = provider.InitFeatures(ctx, license.Features)
if err != nil {
return err
}
return nil
}
@@ -129,28 +125,24 @@ func (provider *provider) GetActive(ctx context.Context, organizationID valuer.U
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil
}
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
return err
}
if err != nil && errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals, defaulting to basic plan", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
if err != nil {
return err
}
return nil
}
return err
@@ -208,73 +200,27 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
// feature surrogate
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.store.GetFeature(ctx, key)
if err != nil {
return err
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
featureStatus, err := provider.store.GetFeature(ctx, key)
if err != nil {
return nil, err
}
return &featuretypes.GettableFeature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
storableFeatures, err := provider.store.GetAllFeatures(ctx)
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
license, err := provider.GetActive(ctx, organizationID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return licensetypes.BasicPlan, nil
}
return nil, err
}
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
for idx, gettableFeature := range storableFeatures {
gettableFeatures[idx] = &featuretypes.GettableFeature{
Name: gettableFeature.Name,
Active: gettableFeature.Active,
Usage: int64(gettableFeature.Usage),
UsageLimit: int64(gettableFeature.UsageLimit),
Route: gettableFeature.Route,
return license.Features, nil
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
activeLicense, err := provider.GetActive(ctx, orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return map[string]any{}, nil
}
return nil, err
}
return gettableFeatures, nil
}
func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
featureStatus := make([]*featuretypes.StorableFeature, len(features))
for i, f := range features {
featureStatus[i] = &featuretypes.StorableFeature{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return provider.store.InitFeatures(ctx, featureStatus)
}
func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
return licensetypes.NewStatsFromLicense(activeLicense), nil
}

View File

@@ -5,8 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -81,106 +79,3 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
return nil
}
func (store *store) ListOrganizations(ctx context.Context) ([]valuer.UUID, error) {
orgIDStrs := make([]string, 0)
err := store.sqlstore.
BunDB().
NewSelect().
Model(new(types.Organization)).
Column("id").
Scan(ctx, &orgIDStrs)
if err != nil {
return nil, err
}
orgIDs := make([]valuer.UUID, len(orgIDStrs))
for idx, orgIDStr := range orgIDStrs {
orgID, err := valuer.NewUUID(orgIDStr)
if err != nil {
return nil, err
}
orgIDs[idx] = orgID
}
return orgIDs, nil
}
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storableFeature).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
}
return nil
}
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
storableFeature := new(featuretypes.StorableFeature)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableFeature).
Where("name = ?", key).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
}
return storableFeature, nil
}
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
storableFeatures := make([]*featuretypes.StorableFeature, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableFeatures).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
}
return storableFeatures, nil
}
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(&storableFeatures).
On("CONFLICT (name) DO UPDATE").
Set("active = EXCLUDED.active").
Set("usage = EXCLUDED.usage").
Set("usage_limit = EXCLUDED.usage_limit").
Set("route = EXCLUDED.route").
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
}
return nil
}
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableFeature).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
}
return nil
}

View File

@@ -39,6 +39,7 @@ builds:
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
- >-
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
mod_timestamp: "{{ .CommitTimestamp }}"

View File

@@ -1,7 +1,6 @@
package api
import (
"context"
"net/http"
"net/http/httputil"
"time"
@@ -13,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/http/middleware"
querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@@ -59,8 +59,9 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
Signoz: signoz,
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
})
if err != nil {
@@ -86,23 +87,12 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
// invite
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
@@ -114,9 +104,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)

View File

@@ -9,9 +9,7 @@ import (
"go.uber.org/zap"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -25,29 +23,11 @@ func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string)
// receiveSAML completes a SAML request and gets user logged in
func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
// this is the source url that initiated the login request
redirectUri := constants.GetDefaultSiteURL()
ctx := context.Background()
_, err = ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return
}
err = r.ParseForm()
err := r.ParseForm()
if err != nil {
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri)
@@ -76,6 +56,19 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
return
}
orgID, err := valuer.NewUUID(domain.OrgID)
if err != nil {
handleSsoError(w, r, redirectUri)
return
}
_, err = ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return
}
sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil {
zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))

View File

@@ -1,62 +0,0 @@
package api
import (
"net/http"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, true)
}
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, false)
}
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
// Locking can only be done by the owner of the dashboard
// or an admin
// - Fetch the dashboard
// - Check if the user is the owner or an admin
// - If yes, lock/unlock the dashboard
// - If no, return 403
// Get the dashboard UUID from the request
uuid := mux.Vars(r)["uuid"]
if strings.HasPrefix(uuid, "integration") {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified"))
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), claims.OrgID, uuid)
if err != nil {
render.Error(w, err)
return
}
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
return
}
// Lock/Unlock the dashboard
err = ah.Signoz.Modules.Dashboard.LockUnlock(r.Context(), claims.OrgID, uuid, lock)
if err != nil {
render.Error(w, err)
return
}
ah.Respond(w, "Dashboard updated successfully")
}

View File

@@ -12,7 +12,7 @@ import (
pkgError "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
@@ -31,7 +31,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
return
}
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
@@ -61,7 +61,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
if ah.opts.PreferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
}
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
@@ -72,7 +80,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) {
func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
@@ -131,28 +139,28 @@ func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature,
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data []*featuretypes.GettableFeature `json:"data"`
Status string `json:"status"`
Data []*licensetypes.Feature `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
// Create a map to store the merged features
featureMap := make(map[string]*featuretypes.GettableFeature)
featureMap := make(map[string]*licensetypes.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name] = feature
featureMap[feature.Name.StringValue()] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name] = feature
featureMap[feature.Name.StringValue()] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures []*featuretypes.GettableFeature
var mergedFeatures []*licensetypes.Feature
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}

View File

@@ -3,78 +3,79 @@ package api
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures []*featuretypes.GettableFeature
internalFeatures []*featuretypes.GettableFeature
expected []*featuretypes.GettableFeature
zeusFeatures []*licensetypes.Feature
internalFeatures []*licensetypes.Feature
expected []*licensetypes.Feature
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{},
zeusFeatures: []*licensetypes.Feature{},
internalFeatures: []*licensetypes.Feature{},
expected: []*licensetypes.Feature{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
internalFeatures: []*licensetypes.Feature{},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
zeusFeatures: []*licensetypes.Feature{},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature3"), Active: false},
},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature2"), Active: true},
{Name: valuer.NewString("Feature4"), Active: false},
},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
{Name: "Feature4", Active: false},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: true},
{Name: valuer.NewString("Feature3"), Active: false},
{Name: valuer.NewString("Feature4"), Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: false},
{Name: valuer.NewString("Feature3"), Active: true},
},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
{Name: valuer.NewString("Feature3"), Active: true},
},
},
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -113,6 +114,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.SigNoz.SQLStore,
serverOptions.SigNoz.TelemetryStore,
serverOptions.SigNoz.Prometheus,
serverOptions.SigNoz.Modules.OrgGetter,
)
if err != nil {
@@ -157,7 +159,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.Organization)
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.OrgGetter)
if err != nil {
return nil, err
}
@@ -225,7 +227,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
&opAmpModel.AllAgents, agentConfMgr,
)
orgs, err := apiHandler.Signoz.Modules.Organization.GetAll(context.Background())
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
if err != nil {
return nil, err
}
@@ -240,11 +242,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
r := baseapp.NewRouter()
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -275,8 +276,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
r := baseapp.NewRouter()
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -293,10 +294,12 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterQueryRangeV5Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
@@ -450,6 +453,7 @@ func makeRulesManager(
sqlstore sqlstore.SQLStore,
telemetryStore telemetrystore.TelemetryStore,
prometheus prometheus.Prometheus,
orgGetter organization.Getter,
) (*baserules.Manager, error) {
// create manager opts
managerOpts := &baserules.ManagerOptions{
@@ -465,6 +469,7 @@ func makeRulesManager(
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
SQLStore: sqlstore,
OrgGetter: orgGetter,
}
// create Manager

View File

@@ -4,6 +4,10 @@ import (
"os"
)
const (
DefaultSiteURL = "https://localhost:8080"
)
var LicenseSignozIo = "https://license.signoz.io/api/v1"
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
@@ -20,3 +24,22 @@ func GetOrDefaultEnv(key string, fallback string) string {
}
return v
}
// constant functions that override env vars
// GetDefaultSiteURL returns default site url, primarily
// used to send saml request and allowing backend to
// handle http redirect
func GetDefaultSiteURL() string {
return GetOrDefaultEnv("SIGNOZ_SITE_URL", DefaultSiteURL)
}
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
var IsDotMetricsEnabled = false
func init() {
if GetOrDefaultEnv(DotMetricsEnabled, "false") == "true" {
IsDotMetricsEnabled = true
}
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/config/fileprovider"
"github.com/SigNoz/signoz/pkg/factory"
pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -133,8 +134,8 @@ func main() {
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus)
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),

View File

@@ -41,16 +41,16 @@ type Manager struct {
zeus zeus.Zeus
organizationModule organization.Module
orgGetter organization.Getter
}
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, organizationModule organization.Module) (*Manager, error) {
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter) (*Manager, error) {
m := &Manager{
clickhouseConn: clickhouseConn,
licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
zeus: zeus,
organizationModule: organizationModule,
clickhouseConn: clickhouseConn,
licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
zeus: zeus,
orgGetter: orgGetter,
}
return m, nil
}
@@ -74,8 +74,7 @@ func (lm *Manager) Start(ctx context.Context) error {
return nil
}
func (lm *Manager) UploadUsage(ctx context.Context) {
organizations, err := lm.organizationModule.GetAll(context.Background())
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
zap.L().Error("failed to get organizations", zap.Error(err))
return

View File

@@ -1,6 +1,7 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
FRONTEND_API_ENDPOINT="http://localhost:8080/"
INTERCOM_APP_ID="intercom-app-id"
PYLON_APP_ID="pylon-app-id"
APPCUES_APP_ID="appcess-app-id"
CI="1"

View File

@@ -134,7 +134,7 @@
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^4.15.2",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@@ -197,7 +197,6 @@
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"critters-webpack-plugin": "^3.0.1",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
@@ -235,7 +234,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2"
"webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [

View File

@@ -1,3 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or "
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via chat support or "
}

View File

@@ -1,3 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support or "
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via chat support or "
}

View File

@@ -78,7 +78,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const checkFirstTimeUser = useCallback((): boolean => {
const users = usersData?.data || [];
const remainingUsers = users.filter(
const remainingUsers = (Array.isArray(users) ? users : []).filter(
(user) => user.email !== 'admin@signoz.cloud',
);
@@ -95,7 +95,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
(preference: Record<string, any>) => preference.name === 'org_onboarding',
)?.value;
const isFirstUser = checkFirstTimeUser();

View File

@@ -28,6 +28,7 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { Userpilot } from 'userpilot';
import { extractDomain } from 'utils/app';
@@ -103,6 +104,20 @@ function App(): JSX.Element {
if (domain) {
logEvent('Domain Identified', groupTraits, 'group');
}
if (window && window.Appcues) {
window.Appcues.identify(email, {
name: displayName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
companyName: orgName,
email,
paidUser: !!trialInfo?.trialConvertedToSubscription,
});
}
Userpilot.identify(email, {
email,
@@ -137,18 +152,6 @@ function App(): JSX.Element {
source: 'signoz-ui',
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
if (
window.cioanalytics &&
typeof window.cioanalytics.identify === 'function'
) {
window.cioanalytics.reset();
window.cioanalytics.identify(email, {
name: user.displayName,
email,
role: user.role,
});
}
}
},
[
@@ -169,11 +172,13 @@ function App(): JSX.Element {
user &&
!!user.email
) {
// either the active API returns error with 404 or 501 and if it returns a terminated license means it's on basic plan
const isOnBasicPlan =
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
(activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
)) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
@@ -188,6 +193,10 @@ function App(): JSX.Element {
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
@@ -213,13 +222,13 @@ function App(): JSX.Element {
useEffect(() => {
if (pathname === ROUTES.ONBOARDING) {
window.Intercom('update', {
hide_default_launcher: true,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Pylon('hideChatBubble');
} else {
window.Intercom('update', {
hide_default_launcher: false,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Pylon('showChatBubble');
}
}, [pathname]);
@@ -254,11 +263,13 @@ function App(): JSX.Element {
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser)
) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
name: user?.displayName || '',
});
window.pylon = {
chat_settings: {
app_id: process.env.PYLON_APP_ID,
email: user.email,
name: user.displayName,
},
};
}
}
}, [
@@ -320,10 +331,6 @@ function App(): JSX.Element {
} else {
posthog.reset();
Sentry.close();
if (window.cioanalytics && typeof window.cioanalytics.reset === 'function') {
window.cioanalytics.reset();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCloudUser, isEnterpriseSelfHostedUser]);

View File

@@ -1,27 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
const createDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards';
try {
const response = await axios.post(url, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default createDashboard;

View File

@@ -1,9 +0,0 @@
import axios from 'api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = (props: Props): Promise<PayloadProps> =>
axios
.delete<PayloadProps>(`/dashboards/${props.uuid}`)
.then((response) => response.data);
export default deleteDashboard;

View File

@@ -1,11 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const getDashboard = (props: Props): Promise<Dashboard> =>
axios
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
.then((res) => res.data.data);
export default getDashboard;

View File

@@ -1,8 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
export const getAllDashboardList = (): Promise<Dashboard[]> =>
axios
.get<ApiResponse<Dashboard[]>>('/dashboards')
.then((res) => res.data.data);

View File

@@ -1,11 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface LockDashboardProps {
uuid: string;
}
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/lock`);
export default lockDashboard;

View File

@@ -1,11 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface UnlockDashboardProps {
uuid: string;
}
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/unlock`);
export default unlockDashboard;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const updateDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.put(`/dashboards/${props.uuid}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateDashboard;

View File

@@ -1,10 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
const getFeaturesFlags = (): Promise<FeatureFlagProps[]> =>
axios
.get<ApiResponse<FeatureFlagProps[]>>(`/featureFlags`)
.then((response) => response.data.data);
export default getFeaturesFlags;

View File

@@ -4,7 +4,11 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import loginApi from 'api/v1/login/login';
import afterLogin from 'AppRoutes/utils';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import axios, {
AxiosError,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -83,24 +87,27 @@ const interceptorRejected = async (
true,
);
const reResponse = await axios(
`${value.config.baseURL}${value.config.url?.substring(1)}`,
{
method: value.config.method,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessJwt}`,
try {
const reResponse = await axios(
`${value.config.baseURL}${value.config.url?.substring(1)}`,
{
method: value.config.method,
headers: {
...value.config.headers,
Authorization: `Bearer ${response.data.accessJwt}`,
},
data: {
...JSON.parse(value.config.data || '{}'),
},
},
data: {
...JSON.parse(value.config.data || '{}'),
},
},
);
if (reResponse.status === 200) {
);
return await Promise.resolve(reResponse);
} catch (error) {
if ((error as AxiosError)?.response?.status === 401) {
Logout();
}
}
Logout();
return await Promise.reject(reResponse);
} catch (error) {
Logout();
}

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sClustersListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -40,23 +42,80 @@ export interface K8sClustersListResponse {
};
}
export const clustersMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.cluster.uid', under: 'k8s_cluster_uid' },
] as const;
export function mapClustersMeta(
raw: Record<string, unknown>,
): K8sClustersData['meta'] {
const out: Record<string, unknown> = { ...raw };
clustersMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sClustersData['meta'];
}
export const getK8sClustersList = async (
props: K8sClustersListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/clusters/list', props, {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/clusters/list', requestProps, {
signal,
headers,
});
const payload: K8sClustersListResponse = response.data;
// one-liner meta mapping
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapClustersMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sDaemonSetsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -46,23 +48,82 @@ export interface K8sDaemonSetsListResponse {
};
}
export const daemonSetsMetaMap = [
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapDaemonSetsMeta(
raw: Record<string, unknown>,
): K8sDaemonSetsData['meta'] {
const out: Record<string, unknown> = { ...raw };
daemonSetsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sDaemonSetsData['meta'];
}
export const getK8sDaemonSetsList = async (
props: K8sDaemonSetsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sDaemonSetsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/daemonsets/list', props, {
// filter prep (unchanged)…
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/daemonsets/list', requestProps, {
signal,
headers,
});
const payload: K8sDaemonSetsListResponse = response.data;
// single-line meta mapping
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapDaemonSetsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sDeploymentsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -46,23 +48,81 @@ export interface K8sDeploymentsListResponse {
};
}
export const deploymentsMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapDeploymentsMeta(
raw: Record<string, unknown>,
): K8sDeploymentsData['meta'] {
const out: Record<string, unknown> = { ...raw };
deploymentsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sDeploymentsData['meta'];
}
export const getK8sDeploymentsList = async (
props: K8sDeploymentsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/deployments/list', props, {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/deployments/list', requestProps, {
signal,
headers,
});
const payload: K8sDeploymentsListResponse = response.data;
// single-line mapping
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapDeploymentsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sJobsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -48,23 +50,79 @@ export interface K8sJobsListResponse {
};
}
export const jobsMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapJobsMeta(raw: Record<string, unknown>): K8sJobsData['meta'] {
const out: Record<string, unknown> = { ...raw };
jobsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sJobsData['meta'];
}
export const getK8sJobsList = async (
props: K8sJobsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sJobsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/jobs/list', props, {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/jobs/list', requestProps, {
signal,
headers,
});
const payload: K8sJobsListResponse = response.data;
// one-liner meta mapping
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapJobsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sNamespacesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -38,23 +40,79 @@ export interface K8sNamespacesListResponse {
};
}
export const namespacesMetaMap = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapNamespacesMeta(
raw: Record<string, unknown>,
): K8sNamespacesData['meta'] {
const out: Record<string, unknown> = { ...raw };
namespacesMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sNamespacesData['meta'];
}
export const getK8sNamespacesList = async (
props: K8sNamespacesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sNamespacesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/namespaces/list', props, {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/namespaces/list', requestProps, {
signal,
headers,
});
const payload: K8sNamespacesListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapNamespacesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sNodesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -41,23 +43,81 @@ export interface K8sNodesListResponse {
};
}
export const nodesMetaMap = [
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{ dot: 'k8s.node.uid', under: 'k8s_node_uid' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapNodesMeta(
raw: Record<string, unknown>,
): K8sNodesData['meta'] {
const out: Record<string, unknown> = { ...raw };
nodesMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sNodesData['meta'];
}
export const getK8sNodesList = async (
props: K8sNodesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/nodes/list', props, {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/nodes/list', requestProps, {
signal,
headers,
});
const payload: K8sNodesListResponse = response.data;
// one-liner to map dot→underscore
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapNodesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sPodsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -69,23 +71,87 @@ export interface K8sPodsListResponse {
};
}
export const podsMetaMap = [
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
] as const;
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
// clone everything
const out: Record<string, unknown> = { ...raw };
// overlay only the dot→under mappings
podsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sPodsData['meta'];
}
// getK8sPodsList
export const getK8sPodsList = async (
props: K8sPodsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/pods/list', props, {
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({
...item,
key: { ...item.key, key: mappedKey },
});
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/pods/list', requestProps, {
signal,
headers,
});
const payload: K8sPodsListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapPodsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sVolumesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -47,23 +49,92 @@ export interface K8sVolumesListResponse {
};
}
export const volumesMetaMap: Array<{
dot: keyof Record<string, unknown>;
under: keyof K8sVolumesData['meta'];
}> = [
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
{
dot: 'k8s.persistentvolumeclaim.name',
under: 'k8s_persistentvolumeclaim_name',
},
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
];
export function mapVolumesMeta(
rawMeta: Record<string, unknown>,
): K8sVolumesData['meta'] {
// start with everything that was already there
const out: Record<string, unknown> = { ...rawMeta };
// for each dot→under rule, if the raw has the dot, overwrite the underscore
volumesMetaMap.forEach(({ dot, under }) => {
if (dot in rawMeta) {
const val = rawMeta[dot];
out[under] = typeof val === 'string' ? val : rawMeta[under];
}
});
return out as K8sVolumesData['meta'];
}
export const getK8sVolumesList = async (
props: K8sVolumesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sVolumesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/pvcs/list', props, {
// Prepare filters
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/pvcs/list', requestProps, {
signal,
headers,
});
const payload: K8sVolumesListResponse = response.data;
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapVolumesMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -5,6 +5,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { UnderscoreToDotMap } from '../utils';
export interface K8sStatefulSetsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
@@ -45,23 +47,78 @@ export interface K8sStatefulSetsListResponse {
};
}
export const statefulSetsMetaMap = [
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
] as const;
export function mapStatefulSetsMeta(
raw: Record<string, unknown>,
): K8sStatefulSetsData['meta'] {
const out: Record<string, unknown> = { ...raw };
statefulSetsMetaMap.forEach(({ dot, under }) => {
if (dot in raw) {
const v = raw[dot];
out[under] = typeof v === 'string' ? v : raw[under];
}
});
return out as K8sStatefulSetsData['meta'];
}
export const getK8sStatefulSetsList = async (
props: K8sStatefulSetsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/statefulsets/list', props, {
// Prepare filters
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
? {
...props,
filters: {
...props.filters,
items: props.filters.items.reduce<typeof props.filters.items>(
(acc, item) => {
if (item.value === undefined) return acc;
if (
item.key &&
typeof item.key === 'object' &&
'key' in item.key &&
typeof item.key.key === 'string'
) {
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
} else {
acc.push(item);
}
return acc;
},
[] as typeof props.filters.items,
),
},
}
: props;
const response = await axios.post('/statefulsets/list', requestProps, {
signal,
headers,
});
const payload: K8sStatefulSetsListResponse = response.data;
// apply our helper
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapStatefulSetsMeta(record.meta as Record<string, unknown>),
}));
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
payload,
params: requestProps,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllOrgPreferences = async (): Promise<
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/org/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllOrgPreferences;

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllUserPreferences = async (): Promise<
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllUserPreferences;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getOrgPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
const response = await axios.get(`/org/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getOrgPreference;

View File

@@ -1,22 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getUserPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getUserPreference;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateOrgPreferenceProps,
UpdateOrgPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateOrgPreference = async (
preferencePayload: UpdateOrgPreferenceProps,
): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(
`/org/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateOrgPreference;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateUserPreferenceProps,
UpdateUserPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateUserPreference = async (
preferencePayload: UpdateUserPreferenceProps,
): Promise<
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(
`/user/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateUserPreference;

View File

@@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload {
export const saveFunnelDescription = async (
payload: UpdateFunnelDescriptionPayload,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/save`,
const response: AxiosResponse = await axios.put(
`${FUNNELS_BASE_PATH}/${payload.funnel_id}`,
payload,
);
@@ -196,8 +196,6 @@ export interface FunnelOverviewResponse {
avg_rate: number;
conversion_rate: number | null;
errors: number;
// TODO(shaheer): remove p99_latency once we have support for latency
p99_latency: number;
latency: number;
};
}>;

View File

@@ -15,13 +15,21 @@ export const Logout = (): void => {
deleteLocalStorageKey(LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT);
window.dispatchEvent(new CustomEvent('LOGOUT'));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (window && window.Intercom) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Intercom('shutdown');
}
history.push(ROUTES.LOGIN);
};
export const UnderscoreToDotMap: Record<string, string> = {
k8s_cluster_name: 'k8s.cluster.name',
k8s_cluster_uid: 'k8s.cluster.uid',
k8s_namespace_name: 'k8s.namespace.name',
k8s_node_name: 'k8s.node.name',
k8s_node_uid: 'k8s.node.uid',
k8s_pod_name: 'k8s.pod.name',
k8s_pod_uid: 'k8s.pod.uid',
k8s_deployment_name: 'k8s.deployment.name',
k8s_daemonset_name: 'k8s.daemonset.name',
k8s_statefulset_name: 'k8s.statefulset.name',
k8s_cronjob_name: 'k8s.cronjob.name',
k8s_job_name: 'k8s.job.name',
k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
};

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
import { Dashboard } from 'types/api/dashboard/getAll';
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.post<PayloadProps>('/dashboards', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default create;

View File

@@ -0,0 +1,19 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
try {
const response = await axios.get<PayloadProps>('/dashboards');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getAll;

View File

@@ -0,0 +1,21 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = async (
props: Props,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteDashboard;

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put<PayloadProps>(
`/dashboards/${props.id}/lock`,
{ lock: props.lock },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default lock;

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
...props.data,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
FeatureFlagProps,
PayloadProps,
} from 'types/api/features/getFeaturesFlags';
const list = async (): Promise<SuccessResponseV2<FeatureFlagProps[]>> => {
try {
const response = await axios.get<PayloadProps>(`/features`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { OrgPreference } from 'types/api/preferences/preference';
const listPreference = async (): Promise<
SuccessResponseV2<OrgPreference[]>
> => {
try {
const response = await axios.get<PayloadProps>(`/org/preferences`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default listPreference;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { OrgPreference } from 'types/api/preferences/preference';
const getPreference = async (
props: Props,
): Promise<SuccessResponseV2<OrgPreference>> => {
try {
const response = await axios.get<PayloadProps>(
`/org/preferences/${props.name}`,
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPreference;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/org/preferences/${props.name}`, {
value: props.value,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getUserPreference';
const getPreference = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getPreference;

View File

@@ -0,0 +1,21 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { UserPreference } from 'types/api/preferences/preference';
const list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
try {
const response = await axios.get<PayloadProps>(`/user/preferences`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { UserPreference } from 'types/api/preferences/preference';
const get = async (
props: Props,
): Promise<SuccessResponseV2<UserPreference>> => {
try {
const response = await axios.get<PayloadProps>(
`/user/preferences/${props.name}`,
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/user/preferences/${props.name}`, {
value: props.value,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -93,7 +93,7 @@
padding: 4px;
margin: 0;
// this is to offset intercom icon
// this is to offset chat support icon
padding-right: 72px;
.ant-pagination-item {

View File

@@ -2,7 +2,7 @@ import { Button, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { CreditCard, MessageSquareText, X } from 'lucide-react';
import { useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
@@ -49,7 +49,7 @@ export default function ChatSupportGateway(): JSX.Element {
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`,
source: `chat support icon`,
page: pathname,
});
@@ -65,20 +65,14 @@ export default function ChatSupportGateway(): JSX.Element {
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `intercom icon`,
source: `chat support icon`,
page: pathname,
});
setIsAddCreditCardModalOpen(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 32"
className="chat-support-gateway-btn-icon"
>
<path d="M28 32s-4.714-1.855-8.527-3.34H3.437C1.54 28.66 0 27.026 0 25.013V3.644C0 1.633 1.54 0 3.437 0h21.125c1.898 0 3.437 1.632 3.437 3.645v18.404H28V32zm-4.139-11.982a.88.88 0 00-1.292-.105c-.03.026-3.015 2.681-8.57 2.681-5.486 0-8.517-2.636-8.571-2.684a.88.88 0 00-1.29.107 1.01 1.01 0 00-.219.708.992.992 0 00.318.664c.142.128 3.537 3.15 9.762 3.15 6.226 0 9.621-3.022 9.763-3.15a.992.992 0 00.317-.664 1.01 1.01 0 00-.218-.707z" />
</svg>
<MessageSquareText size={24} />
</Button>
</div>

View File

@@ -7,9 +7,11 @@ import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { isValidTimeFormat } from 'lib/getMinMax';
@@ -56,6 +58,10 @@ interface CustomTimePickerProps {
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
handleGoLive?: () => void;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
function CustomTimePicker({
@@ -73,6 +79,7 @@ function CustomTimePicker({
setCustomDTPickerVisible,
onCustomDateHandler,
handleGoLive,
onTimeChange,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
@@ -336,6 +343,7 @@ function CustomTimePicker({
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
onTimeChange={onTimeChange}
/>
) : (
content
@@ -405,4 +413,5 @@ CustomTimePicker.defaultProps = {
onCustomDateHandler: noop,
handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
onTimeChange: undefined,
};

View File

@@ -7,9 +7,11 @@ import cx from 'classnames';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
CustomTimeType,
LexicalContext,
Option,
RelativeDurationSuggestionOptions,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
@@ -35,6 +37,10 @@ interface CustomTimePickerPopoverContentProps {
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -51,6 +57,7 @@ function CustomTimePickerPopoverContent({
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
onTimeChange,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
@@ -143,6 +150,7 @@ function CustomTimePickerPopoverContent({
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
onTimeChange={onTimeChange}
/>
) : (
<div className="relative-times-container">
@@ -181,4 +189,8 @@ function CustomTimePickerPopoverContent({
);
}
CustomTimePickerPopoverContent.defaultProps = {
onTimeChange: undefined,
};
export default CustomTimePickerPopoverContent;

View File

@@ -1,9 +1,14 @@
/* eslint-disable react/jsx-props-no-spreading */
import './RangePickerModal.styles.scss';
import { DatePicker } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import {
CustomTimeType,
LexicalContext,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';
@@ -19,6 +24,10 @@ interface RangePickerModalProps {
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
@@ -27,6 +36,7 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
setIsOpen,
onCustomDateHandler,
selectedTime,
onTimeChange,
} = props;
const { RangePicker } = DatePicker;
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
@@ -74,13 +84,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
date.tz(timezone.value).format(DATE_TIME_FORMATS.ISO_DATETIME)
}
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
value: rangeValue,
})}
{...(selectedTime === 'custom' &&
!onTimeChange && {
value: rangeValue,
})}
// use default value if onTimeChange is provided
{...(selectedTime === 'custom' &&
onTimeChange && {
defaultValue: rangeValue,
})}
/>
</div>
);
}
RangePickerModal.defaultProps = {
onTimeChange: undefined,
};
export default RangePickerModal;

View File

@@ -26,6 +26,7 @@ import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { VIEWS } from '../constants';
import { getHostTracesQueryPayload, selectedColumns } from './constants';
import { getListColumns } from './utils';
@@ -39,7 +40,10 @@ interface Props {
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
handleChangeTracesFilters: (
value: IBuilderQuery['filters'],
view: VIEWS,
) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
@@ -70,14 +74,16 @@ function HostMetricTraces({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
items: tracesFilters.items.filter(
(item) => item.key?.key !== 'host.name',
),
op: 'AND',
},
},
],
},
}),
[currentQuery],
[currentQuery, tracesFilters.items],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@@ -153,14 +159,16 @@ function HostMetricTraces({
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeTracesFilters}
onChange={(value): void =>
handleChangeTracesFilters(value, VIEWS.TRACES)
}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showAutoRefresh
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}

View File

@@ -19,6 +19,8 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils';
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
import {
CustomTimeType,
Time,
@@ -93,8 +95,18 @@ function HostMetricsDetails({
);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
const initialFilters = useMemo(() => {
const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW);
const queryKey =
urlView === VIEW_TYPES.LOGS
? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS
: INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS;
const filters = getFiltersFromParams(searchParams, queryKey);
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
@@ -111,9 +123,8 @@ function HostMetricsDetails({
value: host?.hostName || '',
},
],
}),
[host?.hostName],
);
};
}, [host?.hostName, searchParams]);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
@@ -154,7 +165,13 @@ function HostMetricsDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
if (host?.hostName) {
setSearchParams({ hostName: host?.hostName, view: e.target.value });
setSelectedView(e.target.value);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
});
}
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.HostEntity,
@@ -191,7 +208,7 @@ function HostMetricsDetails({
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
@@ -209,7 +226,7 @@ function HostMetricsDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
hostNameFilter,
@@ -217,6 +234,15 @@ function HostMetricsDetails({
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -224,7 +250,7 @@ function HostMetricsDetails({
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
(value: IBuilderQuery['filters'], view: VIEWS) => {
setTracesFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
@@ -238,13 +264,23 @@ function HostMetricsDetails({
});
}
return {
const updatedFilters = {
op: 'AND',
items: [
hostNameFilter,
...value.items.filter((item) => item.key?.key !== 'host.name'),
].filter((item): item is TagFilterItem => item !== undefined),
};
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(
updatedFilters,
),
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
});
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -11,6 +11,7 @@ import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { VIEWS } from '../constants';
import HostMetricsLogs from './HostMetricsLogs';
interface Props {
@@ -23,7 +24,7 @@ interface Props {
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters'], view: VIEWS) => void;
logFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
@@ -51,14 +52,14 @@ function HostMetricLogsDetailedView({
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
items: logFilters.items.filter((item) => item.key?.key !== 'host.name'),
op: 'AND',
},
},
],
},
}),
[currentQuery],
[currentQuery, logFilters.items],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
@@ -70,14 +71,14 @@ function HostMetricLogsDetailedView({
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeLogFilters}
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showAutoRefresh
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}

View File

@@ -18,11 +18,14 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
interface MetricsTabProps {
timeRange: {
startTime: number;
@@ -45,9 +48,20 @@ function Metrics({
handleTimeChange,
isModalTimeSelection,
}: MetricsTabProps): JSX.Element {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryPayloads = useMemo(
() => getHostQueryPayload(hostName, timeRange.startTime, timeRange.endTime),
[hostName, timeRange.startTime, timeRange.endTime],
() =>
getHostQueryPayload(
hostName,
timeRange.startTime,
timeRange.endTime,
dotMetricsEnabled,
),
[hostName, timeRange.startTime, timeRange.endTime, dotMetricsEnabled],
);
const queries = useQueries(
@@ -68,6 +82,45 @@ function Metrics({
[queries],
);
const [graphTimeIntervals, setGraphTimeIntervals] = useState<
{
start: number;
end: number;
}[]
>(
new Array(queries.length).fill({
start: timeRange.startTime,
end: timeRange.endTime,
}),
);
useEffect(() => {
setGraphTimeIntervals(
new Array(queries.length).fill({
start: timeRange.startTime,
end: timeRange.endTime,
}),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeRange]);
const onDragSelect = useCallback(
(start: number, end: number, graphIndex: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
setGraphTimeIntervals((prev) => {
const newIntervals = [...prev];
newIntervals[graphIndex] = {
start: Math.floor(startTimestamp / 1000),
end: Math.floor(endTimestamp / 1000),
};
return newIntervals;
});
},
[],
);
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
@@ -78,12 +131,12 @@ function Metrics({
yAxisUnit: hostWidgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: timeRange.startTime,
maxTimeScale: timeRange.endTime,
enableZoom: true,
minTimeScale: graphTimeIntervals[idx].start,
maxTimeScale: graphTimeIntervals[idx].end,
onDragSelect: (start, end) => onDragSelect(start, end, idx),
}),
),
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
[queries, isDarkMode, dimensions, graphTimeIntervals, onDragSelect],
);
const renderCardContent = (

View File

@@ -24,7 +24,7 @@ export interface LaunchChatSupportProps {
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
chatMessageDisabled?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -35,7 +35,7 @@ function LaunchChatSupport({
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
chatMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { notifications } = useNotifications();
@@ -111,8 +111,10 @@ function LaunchChatSupport({
setIsAddCreditCardModalOpen(true);
} else {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
if (window.pylon && !chatMessageDisabled) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Pylon('showNewMessage', defaultTo(message, ''));
}
}
};
@@ -220,7 +222,7 @@ LaunchChatSupport.defaultProps = {
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
chatMessageDisabled: false,
};
export default LaunchChatSupport;

View File

@@ -16,6 +16,7 @@ import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import {
aggregateAttributesResourcesToString,
escapeHtml,
removeEscapeCharacters,
unescapeString,
} from 'container/LogDetailedView/utils';
@@ -118,7 +119,7 @@ function LogDetail({
const htmlBody = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(log?.body || ''), {
dompurify.sanitize(unescapeString(escapeHtml(log?.body || '')), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),

View File

@@ -7,7 +7,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 { unescapeString } from 'container/LogDetailedView/utils';
import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -58,7 +58,7 @@ function LogGeneralField({
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(fieldValue), {
dompurify.sanitize(unescapeString(escapeHtml(fieldValue)), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),

View File

@@ -5,7 +5,7 @@ import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { unescapeString } from 'container/LogDetailedView/utils';
import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -177,7 +177,7 @@ function RawLogView({
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(text), {
dompurify.sanitize(unescapeString(escapeHtml(text)), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),

View File

@@ -30,6 +30,7 @@
.right-action {
display: flex;
align-items: center;
min-width: 48px;
.clear-all {
font-size: 12px;
@@ -52,10 +53,14 @@
.checkbox-value-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
width: calc(100% - 24px);
cursor: pointer;
.value-string {
width: 100%;
}
&.filter-disabled {
cursor: not-allowed;
@@ -74,9 +79,6 @@
}
}
.value-string {
}
.only-btn {
display: none;
}
@@ -177,3 +179,17 @@
}
}
}
.label-false {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--bg-cherry-500);
}
.label-true {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--bg-forest-500);
}

View File

@@ -504,6 +504,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
onChange(value, currentFilterState[value], true);
}}
>
<div className={`${filter.title} label-${value}`} />
{filter.customRendererForValue ? (
filter.customRendererForValue(value)
) : (
@@ -511,7 +512,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
className="value-string"
ellipsis={{ tooltip: { placement: 'right' } }}
>
{value}
{String(value)}
</Typography.Text>
)}
<Button type="text" className="only-btn">

View File

@@ -0,0 +1,174 @@
.collapseContainer {
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-400);
.ant-collapse-header {
padding: 12px !important;
align-items: center !important;
}
.ant-collapse-expand-icon {
padding-right: 9px !important;
}
.ant-collapse-header-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
.duration-inputs {
display: grid;
gap: 12px;
.min-max-input {
.ant-input-group-addon {
color: var(--bg-vanilla-400);
font-family: 'Space Mono', monospace;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.48px;
padding: 0 6px;
}
.ant-input {
padding: 4px 6px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono', monospace;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.48px;
}
}
}
}
.divider {
background-color: var(--bg-slate-400);
margin: 0;
border-color: var(--bg-slate-400);
}
.filter-header {
padding: 16px 8px 16px 12px;
.filter-title {
display: flex;
gap: 6px;
.ant-typography {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
}
}
.sync-icon {
background-color: var(--bg-ink-500);
border: 0;
box-shadow: none;
padding: 8px;
}
.arrow-icon {
background-color: var(--bg-ink-500);
border: 0;
box-shadow: none;
padding-top: 8px;
.anticon-vertical-align-top {
svg {
width: 16px;
height: 16px;
}
}
}
}
.section-body-header {
display: flex;
> button {
position: absolute;
right: 4px;
padding-top: 13px;
}
.ant-collapse {
width: 100%;
}
}
.section-card {
background-color: var(--bg-ink-500);
.ant-card-body {
padding: 0;
display: flex;
flex-direction: column;
max-height: 500px;
overflow-x: hidden;
overflow-y: auto;
}
width: 240px;
}
.lightMode {
.collapseContainer {
background-color: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-collapse-header-text {
color: var(--bg-slate-100);
}
.duration-inputs {
.min-max-input {
.ant-input-group-addon {
color: var(--bg-slate-100);
}
.ant-input {
color: var(--bg-slate-100);
}
}
}
}
.divider {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-200);
}
.filter-header {
.filter-title {
.ant-typography {
color: var(--bg-slate-100);
}
}
.arrow-icon {
background-color: var(--bg-vanilla-100);
}
.sync-icon {
background-color: var(--bg-vanilla-100);
}
}
.section-card {
background-color: var(--bg-vanilla-100);
box-shadow: none;
}
}

View File

@@ -0,0 +1,281 @@
/* eslint-disable react-hooks/exhaustive-deps */
import './Duration.styles.scss';
import { Button, Collapse } from 'antd';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { DurationSection } from 'pages/TracesExplorer/Filter/DurationSection';
import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
HandleRunProps,
unionTagFilterItems,
} from 'pages/TracesExplorer/Filter/filterUtils';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
function Duration({
filter,
onFilterChange,
}: {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
}): JSX.Element {
const [selectedFilters, setSelectedFilters] = useState<
Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>
>();
const [activeKeys, setActiveKeys] = useState<string[]>([
filter.defaultOpen ? 'durationNano' : '',
]);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const compositeQuery = useGetCompositeQueryParam();
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {
const filters = compositeQuery?.builder.queryData?.[0].filters;
if (!filters) {
return {} as FilterType;
}
return (filters.items || [])
.filter((item) =>
Object.keys(AllTraceFilterKeyValue).includes(item.key?.key as string),
)
.filter(
(item) =>
(item.op === 'in' && item.key?.key !== 'durationNano') ||
(item.key?.key === 'durationNano' && ['>=', '<='].includes(item.op)),
)
.reduce((acc, item) => {
const keys = item.key as BaseAutocompleteData;
const attributeName = item.key?.key || '';
const values = item.value as string[];
if ((attributeName as AllTraceFilterKeys) === 'durationNano') {
if (item.op === '>=') {
acc.durationNanoMin = {
values: getMs(String(values)),
keys,
};
} else {
acc.durationNanoMax = {
values: getMs(String(values)),
keys,
};
}
return acc;
}
if (attributeName) {
if (acc[attributeName as AllTraceFilterKeys]) {
const existingValue = acc[attributeName as AllTraceFilterKeys];
acc[attributeName as AllTraceFilterKeys] = {
values: [...existingValue.values, ...values],
keys,
};
} else {
acc[attributeName as AllTraceFilterKeys] = { values, keys };
}
}
return acc;
}, {} as FilterType);
}, [compositeQuery]);
useEffect(() => {
if (!isEqual(syncSelectedFilters, selectedFilters)) {
setSelectedFilters(syncSelectedFilters);
}
}, [syncSelectedFilters]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const preparePostData = (): TagFilterItem[] => {
if (!selectedFilters) {
return [];
}
const items = Object.keys(selectedFilters)?.flatMap((attribute) => {
const { keys, values } = selectedFilters[attribute as AllTraceFilterKeys];
if (
['durationNanoMax', 'durationNanoMin', 'durationNano'].includes(
attribute as AllTraceFilterKeys,
)
) {
if (!values || !values.length) {
return [];
}
let minValue = '';
let maxValue = '';
const durationItems: TagFilterItem[] = [];
if (isArray(values)) {
minValue = values?.[0];
maxValue = values?.[1];
const minItems: TagFilterItem = {
id: uuid().slice(0, 8),
op: '>=',
key: keys,
value: Number(minValue) * 1000000,
};
const maxItems: TagFilterItem = {
id: uuid().slice(0, 8),
op: '<=',
key: keys,
value: Number(maxValue) * 1000000,
};
return maxValue ? [minItems, maxItems] : [minItems];
}
if (attribute === 'durationNanoMin') {
durationItems.push({
id: uuid().slice(0, 8),
op: '>=',
key: keys,
value: Number(values) * 1000000,
});
} else {
durationItems.push({
id: uuid().slice(0, 8),
op: '<=',
key: keys,
value: Number(values) * 1000000,
});
}
return durationItems;
}
return {
id: uuid().slice(0, 8),
key: keys,
op: 'in',
value: values,
};
});
return items as TagFilterItem[];
};
const removeFilterItemIds = (query: Query): Query => {
const clonedQuery = cloneDeep(query);
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
...data,
filters: {
...data.filters,
items: data.filters?.items?.map((item) => ({
...item,
id: '',
})),
},
}));
return clonedQuery;
};
const handleRun = useCallback(
(props?: HandleRunProps): void => {
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filters: {
...item.filters,
items: props?.resetAll
? []
: (unionTagFilterItems(item.filters?.items, preparePostData())
.map((item) =>
item.key?.key === props?.clearByType ? undefined : item,
)
.filter((i) => i) as TagFilterItem[]),
},
})),
},
};
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
if (
isEqual(currentQueryWithoutIds, preparedQueryWithoutIds) &&
!props?.resetAll
) {
return;
}
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
},
[currentQuery, redirectWithQueryBuilderData, selectedFilters],
);
useEffect(() => {
handleRun();
}, [selectedFilters]);
const onClearHandler = (e: React.MouseEvent): void => {
e.stopPropagation();
e.preventDefault();
if (selectedFilters?.durationNanoMin || selectedFilters?.durationNanoMax) {
handleRun({ clearByType: 'durationNano' });
}
};
return (
<div className="section-body-header" data-testid="collapse-duration">
<Collapse
bordered={false}
className="collapseContainer"
activeKey={activeKeys}
onChange={(keys): void => setActiveKeys(keys as string[])}
items={[
{
key: 'durationNano',
children: (
<DurationSection
setSelectedFilters={setSelectedFilters}
selectedFilters={selectedFilters}
/>
),
label: 'Duration',
},
]}
/>
{activeKeys.includes('durationNano') && (
<Button
type="link"
onClick={onClearHandler}
data-testid="collapse-duration-clearBtn"
>
Clear All
</Button>
)}
</div>
);
}
Duration.defaultProps = {
onFilterChange: (): void => {},
};
export default Duration;

View File

@@ -5,19 +5,24 @@ import {
SyncOutlined,
VerticalAlignTopOutlined,
} from '@ant-design/icons';
import { Skeleton, Tooltip, Typography } from 'antd';
import { Skeleton, Switch, Tooltip, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction, isNull } from 'lodash-es';
import { Settings2 as SettingsIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { USER_ROLES } from 'types/roles';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Duration from './FilterRenderers/Duration/Duration';
import Slider from './FilterRenderers/Slider/Slider';
import useFilterConfig from './hooks/useFilterConfig';
import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip';
@@ -32,8 +37,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source,
onFilterChange,
signal,
showFilterCollapse = true,
showQueryName = true,
} = props;
const { user } = useAppContext();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const isAdmin = user.role === USER_ROLES.ADMIN;
const [params, setParams] = useApiMonitoringParams();
const showIP = params.showIP ?? true;
const {
filterConfig,
@@ -95,36 +106,33 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
};
const lastQueryName =
showQueryName &&
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
return (
<div className="quick-filters-container">
<div className="quick-filters">
{source !== QuickFiltersSource.INFRA_MONITORING &&
source !== QuickFiltersSource.API_MONITORING && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">
{lastQueryName ? 'Filters for' : 'Filters'}
</Typography.Text>
{lastQueryName && (
<Tooltip
title={`Filter currently in sync with query ${lastQueryName}`}
>
<Typography.Text className="sync-tag">
{lastQueryName}
</Typography.Text>
</Tooltip>
)}
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<div className="right-action-icon-container">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</div>
{source !== QuickFiltersSource.INFRA_MONITORING && (
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">
{lastQueryName ? 'Filters for' : 'Filters'}
</Typography.Text>
{lastQueryName && (
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
)}
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<div className="right-action-icon-container">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</div>
</Tooltip>
{showFilterCollapse && (
<Tooltip title="Collapse Filters">
<div className="right-action-icon-container">
<VerticalAlignTopOutlined
@@ -133,38 +141,39 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
</div>
</Tooltip>
{isDynamicFilters && (
<Tooltip title="Settings">
<div
className={classNames('right-action-icon-container', {
active: isSettingsOpen,
})}
>
<SettingsIcon
className="settings-icon"
data-testid="settings-icon"
width={14}
height={14}
onClick={(): void => setIsSettingsOpen(true)}
/>
<AnnouncementTooltip
show={showAnnouncementTooltip}
position={{ top: -5, left: 15 }}
title="Edit your quick filters"
message="You can now customize and re-arrange your quick filters panel. Select the quick filters youd need and hide away the rest for faster exploration."
onClose={(): void => {
setLocalStorageKey(
LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT,
'false',
);
}}
/>
</div>
</Tooltip>
)}
</section>
)}
{isDynamicFilters && isAdmin && (
<Tooltip title="Settings">
<div
className={classNames('right-action-icon-container', {
active: isSettingsOpen,
})}
>
<SettingsIcon
className="settings-icon"
data-testid="settings-icon"
width={14}
height={14}
onClick={(): void => setIsSettingsOpen(true)}
/>
<AnnouncementTooltip
show={showAnnouncementTooltip}
position={{ top: -5, left: 15 }}
title="Edit your quick filters"
message="You can now customize and re-arrange your quick filters panel. Select the quick filters youd need and hide away the rest for faster exploration."
onClose={(): void => {
setLocalStorageKey(
LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT,
'false',
);
}}
/>
</div>
</Tooltip>
)}
</section>
)}
</section>
)}
{isCustomFiltersLoading ? (
<div className="quick-filters-skeleton">
@@ -179,31 +188,51 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
</div>
) : (
<OverlayScrollbar>
<section className="filters">
{filterConfig.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.SLIDER:
return <Slider filter={filter} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
}
})}
</section>
<>
{source === QuickFiltersSource.API_MONITORING && (
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
size="small"
style={{ marginLeft: 'auto' }}
checked={showIP ?? true}
onClick={(): void => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !(showIP ?? true),
});
setParams({ showIP });
}}
/>
</div>
)}
<section className="filters">
{filterConfig.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.DURATION:
return <Duration filter={filter} onFilterChange={onFilterChange} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (
<Checkbox
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
}
})}
</section>
</>
</OverlayScrollbar>
)}
</div>
@@ -235,4 +264,6 @@ QuickFilters.defaultProps = {
onFilterChange: null,
signal: '',
config: [],
showFilterCollapse: true,
showQueryName: true,
};

View File

@@ -3,10 +3,12 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { useMemo } from 'react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
function OtherFiltersSkeleton(): JSX.Element {
return (
@@ -34,6 +36,11 @@ function OtherFilters({
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
}): JSX.Element {
const isLogDataSource = useMemo(
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
[signal],
);
const {
data: suggestionsData,
isFetching: isFetchingSuggestions,
@@ -45,18 +52,39 @@ function OtherFilters({
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal,
enabled: !!signal && isLogDataSource,
},
);
const otherFilters = useMemo(
() =>
suggestionsData?.payload?.attributes?.filter(
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
),
[suggestionsData, addedFilters],
const {
data: aggregateKeysData,
isFetching: isFetchingAggregateKeys,
} = useGetAggregateKeys(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
aggregateOperator: 'noop',
aggregateAttribute: '',
tagType: '',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && !isLogDataSource,
},
);
const otherFilters = useMemo(() => {
let filterAttributes;
if (isLogDataSource) {
filterAttributes = suggestionsData?.payload?.attributes || [];
} else {
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
}
return filterAttributes?.filter(
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
);
}, [suggestionsData, aggregateKeysData, addedFilters, isLogDataSource]);
const handleAddFilter = (filter: FilterType): void => {
setAddedFilters((prev) => [
...prev,
@@ -71,7 +99,8 @@ function OtherFilters({
};
const renderFilters = (): React.ReactNode => {
if (isFetchingSuggestions) return <OtherFiltersSkeleton />;
const isLoading = isFetchingSuggestions || isFetchingAggregateKeys;
if (isLoading) return <OtherFiltersSkeleton />;
if (!otherFilters?.length)
return <div className="no-values-found">No values found</div>;

View File

@@ -7,6 +7,7 @@
background: var(--bg-slate-500);
transition: width 0.05s ease-in-out;
overflow: hidden;
color: var(--bg-vanilla-100);
&.qf-logs-explorer {
height: calc(100vh - 45px);
@@ -16,6 +17,14 @@
height: 100vh;
}
&.qf-api-monitoring {
height: calc(100vh - 45px);
}
&.qf-traces-explorer {
height: calc(100vh - 45px);
}
&.hidden {
width: 0;
}
@@ -172,6 +181,7 @@
.lightMode {
.quick-filters-settings {
background: var(--bg-vanilla-100);
color: var(--bg-slate-500);
.search {
.ant-input {
background-color: var(--bg-vanilla-100);

View File

@@ -1,3 +1,4 @@
import logEvent from 'api/common/logEvent';
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
import axios, { AxiosError } from 'axios';
import { SignalType } from 'components/QuickFilters/types';
@@ -46,6 +47,9 @@ const useQuickFilterSettings = ({
onSuccess: () => {
setIsSettingsOpen(false);
setIsStale(true);
logEvent('Quick Filters Settings: changes saved', {
addedFilters,
});
notifications.success({
message: 'Quick filters updated successfully',
placement: 'bottomRight',

View File

@@ -33,7 +33,7 @@ const useFilterConfig = ({
const isDynamicFilters = useMemo(() => customFilters.length > 0, [
customFilters,
]);
const { isLoading: isCustomFiltersLoading } = useQuery<
const { isFetching: isCustomFiltersLoading } = useQuery<
SuccessResponse<PayloadProps> | ErrorResponse,
Error
>(
@@ -49,10 +49,10 @@ const useFilterConfig = ({
enabled: !!signal && isStale,
},
);
const filterConfig = useMemo(() => getFilterConfig(customFilters, config), [
config,
customFilters,
]);
const filterConfig = useMemo(
() => getFilterConfig(signal, customFilters, config),
[config, customFilters, signal],
);
return {
filterConfig,

View File

@@ -1,6 +1,7 @@
import '@testing-library/jest-dom';
import {
act,
cleanup,
fireEvent,
render,
@@ -8,6 +9,7 @@ import {
waitFor,
} from '@testing-library/react';
import { ENVIRONMENT } from 'constants/env';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
otherFiltersResponse,
@@ -17,6 +19,7 @@ import {
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { USER_ROLES } from 'types/roles';
import QuickFilters from '../QuickFilters';
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
@@ -26,6 +29,21 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
// eslint-disable-next-line sonarjs/no-duplicate-string
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
}));
const userRole = USER_ROLES.ADMIN;
// mock useAppContext
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({ user: { role: userRole } })),
}));
const handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn();
const putHandler = jest.fn();
@@ -163,7 +181,9 @@ describe('Quick Filters with custom filters', () => {
expect(screen.getByText('Filters for')).toBeInTheDocument();
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
await screen.findByText(FILTER_SERVICE_NAME);
await screen.findByText('otel-demo');
const allByText = await screen.findAllByText('otel-demo');
// since 2 filter collapse are open, there are 2 filter items visible
expect(allByText).toHaveLength(2);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
fireEvent.click(icon);
@@ -285,4 +305,59 @@ describe('Quick Filters with custom filters', () => {
);
expect(requestBody.signal).toBe(SIGNAL);
});
// render duration filter
it('should render duration slider for duration_nono filter', async () => {
// Set up fake timers **before rendering**
jest.useFakeTimers();
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
expect(screen.getByText('Duration')).toBeInTheDocument();
// click to open the duration filter
fireEvent.click(screen.getByText('Duration'));
const minDuration = getByTestId('min-input') as HTMLInputElement;
const maxDuration = getByTestId('max-input') as HTMLInputElement;
expect(minDuration).toHaveValue(null);
expect(minDuration).toHaveProperty('placeholder', '0');
expect(maxDuration).toHaveValue(null);
expect(maxDuration).toHaveProperty('placeholder', '100000000');
await act(async () => {
// set values
fireEvent.change(minDuration, { target: { value: '10000' } });
fireEvent.change(maxDuration, { target: { value: '20000' } });
jest.advanceTimersByTime(2000);
});
await waitFor(() => {
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
expect.objectContaining({
builder: {
queryData: expect.arrayContaining([
expect.objectContaining({
filters: expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'durationNano' }),
op: '>=',
value: 10000000000,
}),
expect.objectContaining({
key: expect.objectContaining({ key: 'durationNano' }),
op: '<=',
value: 20000000000,
}),
]),
}),
}),
]),
},
}),
);
});
jest.useRealTimers(); // Clean up
});
});

View File

@@ -5,6 +5,7 @@ import { DataSource } from 'types/common/queryBuilder';
export enum FiltersType {
SLIDER = 'SLIDER',
CHECKBOX = 'CHECKBOX',
DURATION = 'DURATION', // ALIAS FOR DURATION_NANO
}
export enum MinMax {
@@ -42,6 +43,8 @@ export interface IQuickFiltersProps {
onFilterChange?: (query: Query) => void;
signal?: SignalType;
className?: string;
showFilterCollapse?: boolean;
showQueryName?: boolean;
}
export enum QuickFiltersSource {

View File

@@ -1,30 +1,53 @@
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { FiltersType, IQuickFiltersConfig } from './types';
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
const getFilterName = (str: string): string =>
const FILTER_TITLE_MAP: Record<string, string> = {
duration_nano: 'Duration',
hasError: 'Has Error (Status)',
};
const FILTER_TYPE_MAP: Record<string, FiltersType> = {
duration_nano: FiltersType.DURATION,
};
const getFilterName = (str: string): string => {
if (FILTER_TITLE_MAP[str]) {
return FILTER_TITLE_MAP[str];
}
// replace . and _ with space
// capitalize the first letter of each word
str
return str
.replace(/\./g, ' ')
.replace(/_/g, ' ')
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
const getFilterType = (att: FilterType): FiltersType => {
if (FILTER_TYPE_MAP[att.key]) {
return FILTER_TYPE_MAP[att.key];
}
return FiltersType.CHECKBOX;
};
export const getFilterConfig = (
signal?: SignalType,
customFilters?: FilterType[],
config?: IQuickFiltersConfig[],
): IQuickFiltersConfig[] => {
if (!customFilters?.length) {
if (!customFilters?.length || !signal) {
return config || [];
}
return customFilters.map(
(att, index) =>
({
type: FiltersType.CHECKBOX,
type: getFilterType(att),
title: getFilterName(att.key),
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
attributeKey: {
id: att.key,
key: att.key,
@@ -33,7 +56,7 @@ export const getFilterConfig = (
isColumn: att.isColumn,
isJSON: att.isJSON,
},
defaultOpen: index === 0,
defaultOpen: index < 2,
} as IQuickFiltersConfig),
);
};

View File

@@ -26,6 +26,18 @@ export interface UplotProps {
resetScales?: boolean;
}
function isAlignedData(data: unknown): data is uPlot.AlignedData {
return Array.isArray(data) && data.length > 0;
}
function isUplotOptions(options: unknown): options is uPlot.Options {
return options !== null && typeof options === 'object';
}
function isHTMLElement(el: unknown): el is HTMLElement {
return el instanceof HTMLElement;
}
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
(
{ options, data, onDelete, onCreate, resetScales = true },
@@ -78,6 +90,19 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
};
}
if (
!isUplotOptions(propOptionsRef.current) ||
!isAlignedData(propDataRef.current) ||
!isHTMLElement(targetRef.current)
) {
console.error('Uplot: Invalid options, data, or target element', {
options: propOptionsRef.current,
data: propDataRef.current,
target: targetRef.current,
});
return;
}
const newChart = new UPlot(
propOptionsRef.current,
propDataRef.current,

View File

@@ -1,13 +1,12 @@
// keep this consistent with backend constants.go
// keep this consistent with backend plan.go
export enum FeatureKeys {
SSO = 'SSO',
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',
SSO = 'sso',
USE_SPAN_METRICS = 'use_span_metrics',
ONBOARDING = 'onboarding',
CHAT_SUPPORT = 'chat_support',
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
}

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