Compare commits

...

39 Commits

Author SHA1 Message Date
primus-bot[bot]
ae73033826 chore(release): bump to v0.74.0 (#7188) 2025-02-26 18:19:02 +05:30
Shaheer Kochai
da9f112bee fix: fix the issue of service to logs/traces explorer caused by similar start and end timestamps (#7181)
* fix: fix the issue of service to logs/traces explorer caused by similar start and end timestamps

* refactor: extract seconds to milliseconds conversion utility

* docs: add JSDoc for onViewTracePopupClick function

* fix: handle seconds to milli seconds in onErrorTrackHandler

* chore: add jsdoc to onErrorTrackHandler
2025-02-25 14:30:19 +05:30
Yunus M
e18bda8480 feat: show a alert if api takes more than 5 secs (#7112)
* feat: show a banner if api takes more than 5 secs

* Update frontend/src/container/AppLayout/index.tsx

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

* feat: show a banner if api takes more than 5 secs

* feat: show toast message with upgrade option

* feat: log api delays

* feat: igmore /events calls

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-21 14:50:29 +05:30
Raj Kamal Singh
bc907b9e61 chore: AWS Integration: include CWAgent metrics namespace for ec2 (#7165)
* chore: include CWAgent namespace for ec2 metrics

* chore: update aws integration service metrics connection status queried labels

* chore: fix breaking test
2025-02-21 10:45:56 +05:30
Nityananda Gohain
b6858fbfd9 fix: materialize columns api fixed (#7159) 2025-02-20 14:39:58 +00:00
Yunus M
858944d273 feat: enable ms teams channel for all users (#7144)
* feat: enable ms teams channel for all users

* fix: update copy in test files
2025-02-20 16:12:49 +05:30
Vibhu Pandey
639b9a5a8a feat(legacyalertmanager): add legacyalertmanager (#7156) 2025-02-20 14:14:09 +05:30
aniketio-ctrl
972a7a9dac feat(summary-view): add summary view endpoints 2025-02-20 13:49:44 +05:30
Shivanshu Raj Shrivastava
407654e68e feat: add APIs for third party api feat (#7005)
* feat: add APIs for third party api feat

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

* fix: minor fixes

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

* test: add unit tests

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

* chore: minor changes

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

* fix: review comments

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

* test: add unit tests

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

* chore: cleanup

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

* chore: review comments

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

* chore: review comments

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

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-02-19 10:38:58 +00:00
primus-bot[bot]
3b6952abf2 chore(release): bump to v0.73.0 (#7153)
#### Summary
 - Release SigNoz v0.73.0
 - Bump SigNoz OTel Collector to v0.111.29

 Created by [Primus-Bot](https://github.com/apps/primus-bot)

Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-02-19 15:29:03 +05:30
Amlan Kumar Nandy
1925b6b4cb chore: reset filters while going from hosts list to host details (#7150) 2025-02-19 14:58:04 +05:30
Vibhu Pandey
3d85fd831a refactor(registry): move into factory (#7147) 2025-02-19 00:35:53 +05:30
Nityananda Gohain
ecbc4acc78 fix: org domain (#7148)
Fixes the updated_at type of org domain
2025-02-18 16:11:37 +00:00
Vibhu Pandey
918c8942c4 feat(alertmanager): add service for alertmanager (#7136)
### Summary

- adds an alertmanager service
2025-02-18 07:36:31 +00:00
Shaheer Kochai
7e1301b8d2 fix: prefer local storage list options over url query options and handle an edge case (#7120)
* fix(LogsExplorer): prefer local storage options of url query options

* fix(LogsExplorer): don't add timestamp/body if local storage selectColumns exist & already migrated

* fix(LogsExplorer): improve query migration logic for local storage options

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-02-18 07:07:14 +00:00
Nityananda Gohain
eba2049a4d fix: signoz shouldn't panic with postgres provider (#7138)
All necessary changes so that whatever initalize SQL commans run, they are moved to bun so that it works with both sqlite and postgres.
2025-02-18 11:08:09 +05:30
Vibhu Pandey
2a939e813d feat(sqlstorehook): add sqlstore logging hook (#7137)
### Summary

add sqlstore logging hook
2025-02-17 21:13:40 +05:30
Nityananda Gohain
c3951afdfd fix: refactor auth package (#7110)
* fix: refactor auth package

* fix: minor changes

* fix: refactor jwt

* fix: add tests and address comments

* fix: address comments

* fix: add uncomitted file

* fix: address comments

* fix: update tests
2025-02-17 18:16:41 +05:30
SagarRajput-7
fbf0e4efc7 fix: fixed paths to unblock lint check failing locally (#7114)
* fix: fixed paths to unblock lint check failing locally

* fix: fixed folder name path

* fix: fixed folder name path - api
2025-02-17 09:51:19 +00:00
SagarRajput-7
1f52139ed3 chore: added kakfa analytics (#7127) 2025-02-17 09:34:57 +00:00
SagarRajput-7
e86c7c970a chore: added mq overview page analytics (#7128) 2025-02-17 14:56:11 +05:30
Nityananda Gohain
8bfca9b564 fix: analytics middleware fixed (#7133) 2025-02-17 14:38:13 +05:30
SagarRajput-7
5a107f33f2 chore: added mq celery analytics (#7129) 2025-02-17 13:59:16 +05:30
Vibhu Pandey
a6cfb63036 fix(alertmanager): fix a flaky test (#7123) 2025-02-16 19:02:33 +05:30
Yunus M
042f31116a update project maintainers (#7116)
Co-authored-by: Shaheer Kochai <ashaheerki@gmail.com>
2025-02-15 13:52:05 +00:00
Srikanth Chekuri
e7587612e7 Revert "fix(otelcol): remove the v2 metrics exporter configuration (#… …7105)" (#7118) 2025-02-15 16:29:56 +05:30
SagarRajput-7
43a1fa53aa fix: fixed selected value getting reset for variables and refetch not going (#7115) 2025-02-15 00:23:33 +05:30
Raj Kamal Singh
966f11fd5c chore: enable AWS integrations for all (#7111)
* chore: enable AWS integrations for all

* chore: don't show either of configure/enable for a service when not in ctx of a cloud account

* chore: remove AWS integration feature flag
2025-02-14 13:19:52 +00:00
Raj Kamal Singh
52f41e0064 Chore: aws integrations: ec2 and rds dashboards and collected metric details with gauges (#7113)
* chore: aws integration: update agent version

* chore: aws integration: gauges based ec2 overview dashboard

* chore: aws integration: gauges based rds overview dashboard

* chore: aws integrations: ec2 metrics collected

* chore: aws integrations: rds metrics collected
2025-02-14 18:40:46 +05:30
Shaheer Kochai
82d84c041c Feat: back navigation support throughout the app (#6701)
* feat: custom hook to prevent redundant navigation and handle default params with URL comparison

* feat: implement useSafeNavigation to QB, to ensure that the back navigation works properly

* fix: handle syncing the relativeTime param with the time picker selected relative time

* feat: add support for absolute and relative time sync with time picker component

* refactor: integrate safeNavigate in LogsExplorerChart and deprecate the existing back navigation

* feat: update pagination query params on pressing next/prev page

* fix: fix the issue of NOOP getting converted to Count on coming back from alert creation page

* refactor: replace history navigation with safeNavigate in DateTimeSelectionV2 component

it also fixes the issue of relativeTime not being added to the url on mounting

* feat: integrate useSafeNavigate across service details tabs

* fix: fix duplicate redirections by converting the timestamp to milliseconds

* fix: replace history navigation with useSafeNavigate in LogsExplorerViews and useUrlQueryData

* fix: replace history navigation with useSafeNavigate across dashboard components

* fix: use safeNavigate in alert components

* fix: fix the issue of back navigation in alert table and sync the pagination with url param

* fix: handle back navigation for resource filter and sync the state with url query

* fix: fix the issue of double redirection from top operations to traces

* fix: replace history.push with safeNavigate in TracesExplorer's updateDashboard

* fix: prevent unnecessary query re-runs by checking stagedQuery before redirecting in NewWidget

* chore: cleanup

* fix: fix the failing tests

* fix: fix the documentation redirection failing tests

* test: mock useSafeNavigate hook in WidgetGraphComponent test

* test: mock useSafeNavigate hook in ExplorerCard test
2025-02-14 03:54:49 +00:00
Raj Kamal Singh
ef635b6b60 Chore: aws integrations UI improvements (#7068)
* chore: handle race between initial setting of ?cloudAccountId and ?service

* chore: invalidate accounts query after successful account connection

* chore: show service status only when enabled and disable save btn only if no change in svc config

* chore: re-trigger CI
2025-02-13 14:32:17 +00:00
Raj Kamal Singh
169bd3f65b Feat: aws integrations: dashboards (#7058)
* chore: get started on dashboards plumbing for AWS integration services

* feat: cloud integrations: include cloud integrations dashboards in dashboards list

* feat: cloud integrations: get cloud integration dashboard by id

* feat: dashboard url in cloud integrations svc detail

* feat: ec2 overview dashboard

* chore: add ec2 overview dashboard image

* chore: finish up with v0 definitions for EC2 and RDS

* chore: some cleanup

* chore: fix broken test

* chore: add connection url composition

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-02-13 14:15:51 +00:00
SagarRajput-7
50ecf768fa fix: fixed widgets not visible after saving in the dashboard detail (#7106) 2025-02-13 17:04:24 +05:30
Vibhu Pandey
2d6131c291 feat(alertmanager): add server implementation for alertmanager (#7103) 2025-02-13 09:08:58 +00:00
Aarif Mohammad
7359c0a825 fix[FE]: fix the update view button not visible on changes to Not Updating in Logs and Traces List View. (#6454)
* fix[FE]: fix the update view button not visible on changes to columns in logs and traces list view

* fix: pass the missing options to isStagedQueryUpdated in ExplorerCard

* test: mock useHistory hook for ExplorerCard tests

---------

Co-authored-by: ahmadshaheer <ashaheerki@gmail.com>
2025-02-13 04:18:18 +00:00
Prashant Shahi
846b6a9346 fix(otelcol): remove the v2 metrics exporter configuration (#7105) 2025-02-13 04:31:25 +05:30
Vibhu Pandey
bcf7bf38fc feat(alertmanager): add alertmanagertypes (#7101)
add alertmanagertypes
2025-02-12 17:23:18 +00:00
Prashant Shahi
5511c0230b fix(otelcol): rename clickhousemetricswritev2 to signozclickhousemetrics (#7099)
### Summary

- rename exporter `clickhousemetricswritev2` to `signozclickhousemetrics` in collector config

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-02-12 19:25:29 +05:30
Nityananda Gohain
7a03a09ac1 fix: add migration and postgres provider (#7089)
* fix: move migrations to bun

* fix: use anonymous structs and move modes to types package

* fix: minor changes after tests

* fix: remove bun relations and add foreign keys

* fix: minor changes

* Update pkg/types/agent.go

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

* fix: add migration and postgres provider

* fix: address minor comments

* fix: use bun create index

* fix: add migration

* fix: support for postgres in migrations

* Update pkg/sqlstore/pgstore/provider.go

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

* Update pkg/sqlmigration/001_add_organization.go

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

* fix: address comments

* fix: move max connection to base config

* fix: update scope

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-12 13:23:40 +00:00
249 changed files with 13088 additions and 1579 deletions

View File

@@ -219,12 +219,18 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
- [Nityananda Gohain](https://github.com/nityanandagohain)
- [Srikanth Chekuri](https://github.com/srikanthccv)
- [Vishal Sharma](https://github.com/makeavish)
- [Shivanshu Raj Shrivastava](https://github.com/shivanshuraj1333)
- [Ekansh Gupta](https://github.com/eKuG)
- [Aniket Agarwal](https://github.com/aniketio-ctrl)
#### Frontend
- [Yunus M](https://github.com/YounixM)
- [Vikrant Gupta](https://github.com/vikrantgupta25)
- [Sagar Rajput](https://github.com/SagarRajput-7)
- [Shaheer Kochai](https://github.com/ahmadshaheer)
- [Amlan Kumar Nandy](https://github.com/amlannandy)
- [Sahil Khan](https://github.com/sawhil)
#### DevOps

View File

@@ -181,7 +181,7 @@ services:
- query-service
query-service:
!!merge <<: *db-depend
image: signoz/query-service:0.72.0
image: signoz/query-service:0.74.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -214,7 +214,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:0.72.0
image: signoz/frontend:0.74.0
depends_on:
- alertmanager
- query-service
@@ -224,7 +224,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.27
image: signoz/signoz-otel-collector:0.111.29
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -248,7 +248,7 @@ services:
- query-service
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.24
image: signoz/signoz-schema-migrator:0.111.29
deploy:
restart_policy:
condition: on-failure

View File

@@ -117,7 +117,7 @@ services:
- query-service
query-service:
!!merge <<: *db-depend
image: signoz/query-service:0.72.0
image: signoz/query-service:0.74.0
command:
- --config=/root/config/prometheus.yml
- --use-logs-new-schema=true
@@ -150,7 +150,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:0.72.0
image: signoz/frontend:0.74.0
depends_on:
- alertmanager
- query-service
@@ -160,7 +160,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:0.111.27
image: signoz/signoz-otel-collector:0.111.29
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -184,7 +184,7 @@ services:
- query-service
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:0.111.24
image: signoz/signoz-schema-migrator:0.111.29
deploy:
restart_policy:
condition: on-failure

View File

@@ -66,7 +66,7 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhousemetricswritev2:
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -90,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

View File

@@ -188,7 +188,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.72.0}
image: signoz/query-service:${DOCKER_TAG:-0.74.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -222,7 +222,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.72.0}
image: signoz/frontend:${DOCKER_TAG:-0.74.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -234,7 +234,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.29}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -260,7 +260,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.29}
container_name: schema-migrator-sync
command:
- sync
@@ -271,7 +271,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.29}
container_name: schema-migrator-async
command:
- async

View File

@@ -121,7 +121,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.72.0}
image: signoz/query-service:${DOCKER_TAG:-0.74.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -157,7 +157,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.72.0}
image: signoz/frontend:${DOCKER_TAG:-0.74.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -168,7 +168,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.29}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -190,7 +190,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.29}
container_name: schema-migrator-sync
command:
- sync
@@ -201,7 +201,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.29}
container_name: schema-migrator-async
command:
- async

View File

@@ -121,7 +121,7 @@ services:
condition: service_healthy
query-service:
!!merge <<: *db-depend
image: signoz/query-service:${DOCKER_TAG:-0.72.0}
image: signoz/query-service:${DOCKER_TAG:-0.74.0}
container_name: signoz-query-service
command:
- --config=/root/config/prometheus.yml
@@ -155,7 +155,7 @@ services:
retries: 3
frontend:
!!merge <<: *common
image: signoz/frontend:${DOCKER_TAG:-0.72.0}
image: signoz/frontend:${DOCKER_TAG:-0.74.0}
container_name: signoz-frontend
depends_on:
- alertmanager
@@ -166,7 +166,7 @@ services:
- ../common/signoz/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.29}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -188,7 +188,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.29}
container_name: schema-migrator-sync
command:
- sync
@@ -199,7 +199,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.29}
container_name: schema-migrator-async
command:
- async

View File

@@ -66,7 +66,7 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhousemetricswritev2:
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -90,11 +90,11 @@ service:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
exporters: [clickhousemetricswrite, signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
exporters: [clickhousemetricswrite/prometheus, signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]

36
ee/http/middleware/pat.go Normal file
View File

@@ -0,0 +1,36 @@
package middleware
import (
"net/http"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type Pat struct {
uuid *authtypes.UUID
headers []string
}
func NewPat(headers []string) *Pat {
return &Pat{uuid: authtypes.NewUUID(), headers: headers}
}
func (p *Pat) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var values []string
for _, header := range p.headers {
values = append(values, r.Header.Get(header))
}
ctx, err := p.uuid.ContextFromRequest(r.Context(), values...)
if err != nil {
next.ServeHTTP(w, r)
return
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

View File

@@ -20,6 +20,7 @@ import (
basemodel "go.signoz.io/signoz/pkg/query-service/model"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type APIHandlerOptions struct {
@@ -41,6 +42,7 @@ type APIHandlerOptions struct {
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
JWT *authtypes.JWT
}
type APIHandler struct {

View File

@@ -50,7 +50,7 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
}
// if all looks good, call auth
resp, err := baseauth.Login(ctx, &req)
resp, err := baseauth.Login(ctx, &req, ah.opts.JWT)
if ah.HandleError(w, err, http.StatusUnauthorized) {
return
}
@@ -253,7 +253,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
if err != nil {
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
@@ -331,7 +331,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
if err != nil {
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)

View File

@@ -37,7 +37,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
return
}
currentUser, err := auth.GetUserFromRequest(r)
currentUser, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, basemodel.UnauthorizedError(fmt.Errorf(
"couldn't deduce current user: %w", err,
@@ -183,7 +183,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
if apiErr != nil {
return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user")
}
newUser.GroupId = viewerGroup.Id
newUser.GroupId = viewerGroup.ID
passwordHash, err := auth.PasswordHash(uuid.NewString())
if err != nil {

View File

@@ -34,7 +34,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
RespondError(w, model.BadRequest(err), nil)
return
}
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -97,7 +97,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
return
}
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -127,7 +127,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,
@@ -147,7 +147,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
id := mux.Vars(r)["id"]
user, err := auth.GetUserFromRequest(r)
user, err := auth.GetUserFromReqContext(r.Context())
if err != nil {
RespondError(w, &model.ApiError{
Typ: model.ErrorUnauthorized,

View File

@@ -1,25 +1,20 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof" // http profiler
"regexp"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/cors"
"github.com/soheilhy/cmux"
eemiddleware "go.signoz.io/signoz/ee/http/middleware"
"go.signoz.io/signoz/ee/query-service/app/api"
"go.signoz.io/signoz/ee/query-service/app/db"
"go.signoz.io/signoz/ee/query-service/auth"
@@ -29,9 +24,8 @@ import (
"go.signoz.io/signoz/ee/query-service/interfaces"
"go.signoz.io/signoz/ee/query-service/rules"
"go.signoz.io/signoz/pkg/http/middleware"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.signoz.io/signoz/pkg/web"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -80,6 +74,7 @@ type ServerOptions struct {
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
Jwt *authtypes.JWT
}
// Server runs HTTP api service
@@ -110,7 +105,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore.SQLxDB())
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, err
}
@@ -133,7 +128,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB())
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore.BunDB())
if err != nil {
return nil, err
}
@@ -202,14 +197,14 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
@@ -269,6 +264,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
GatewayUrl: serverOptions.GatewayUrl,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
JWT: serverOptions.Jwt,
}
apiHandler, err := api.NewAPIHandler(apiOpts)
@@ -311,6 +307,8 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter()
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(eemiddleware.NewPat([]string{"SIGNOZ-API-KEY"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -342,8 +340,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
r := baseapp.NewRouter()
// add auth middleware
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequest(r, apiHandler)
getUserFromRequest := func(ctx context.Context) (*basemodel.UserPayload, error) {
user, err := auth.GetUserFromRequestContext(ctx, apiHandler)
if err != nil {
return nil, err
@@ -357,6 +355,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
r.Use(eemiddleware.NewPat([]string{"SIGNOZ-API-KEY"}).Wrap)
r.Use(middleware.NewTimeout(zap.L(),
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
s.serverOptions.Config.APIServer.Timeout.Default,
@@ -374,6 +374,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
@@ -395,174 +397,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code.
return &loggingResponseWriter{w, http.StatusOK}
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
data := map[string]interface{}{}
var postData *v3.QueryRangeParamsV3
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &postData)
} else {
return nil, false
}
} else {
return nil, false
}
referrer := r.Header.Get("Referer")
dashboardMatched, err := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the referrer", zap.Error(err))
}
alertMatched, err := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the alert: ", zap.Error(err))
}
logsExplorerMatched, err := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the logs explorer: ", zap.Error(err))
}
traceExplorerMatched, err := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(postData)
if (queryInfoResult.MetricsUsed || queryInfoResult.LogsUsed || queryInfoResult.TracesUsed) && (queryInfoResult.FilterApplied) {
if queryInfoResult.MetricsUsed {
telemetry.GetInstance().AddActiveMetricsUser()
}
if queryInfoResult.LogsUsed {
telemetry.GetInstance().AddActiveLogsUser()
}
if queryInfoResult.TracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = queryInfoResult.MetricsUsed
data["logsUsed"] = queryInfoResult.LogsUsed
data["tracesUsed"] = queryInfoResult.TracesUsed
data["filterApplied"] = queryInfoResult.FilterApplied
data["groupByApplied"] = queryInfoResult.GroupByApplied
data["aggregateOperator"] = queryInfoResult.AggregateOperator
data["aggregateAttributeKey"] = queryInfoResult.AggregateAttributeKey
data["numberOfQueries"] = queryInfoResult.NumberOfQueries
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
data["screen"] = "panel"
case alertMatched:
data["screen"] = "alert"
case logsExplorerMatched:
data["screen"] = "logs-explorer"
case traceExplorerMatched:
data["screen"] = "traces-explorer"
default:
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
}
func getActiveLogs(path string, r *http.Request) {
// if path == "/api/v1/dashboards/{uuid}" {
// telemetry.GetInstance().AddActiveMetricsUser()
// }
if path == "/api/v1/logs" {
hasFilters := len(r.URL.Query().Get("q"))
if hasFilters > 0 {
telemetry.GetInstance().AddActiveLogsUser()
}
}
}
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := baseauth.AttachJwtToContext(r.Context(), r)
r = r.WithContext(ctx)
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeData, metadataExists := extractQueryRangeData(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
next.ServeHTTP(lrw, r)
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range queryRangeData {
data[key] = value
}
}
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}
})
}
// initListeners initialises listeners of the server
func (s *Server) initListeners() error {
// listen on public port

View File

@@ -3,20 +3,20 @@ package auth
import (
"context"
"fmt"
"net/http"
"time"
"go.signoz.io/signoz/ee/query-service/app/api"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/telemetry"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken := r.Header.Get("SIGNOZ-API-KEY")
if len(patToken) > 0 {
func GetUserFromRequestContext(ctx context.Context, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken, ok := authtypes.UUIDFromContext(ctx)
if ok && patToken != "" {
zap.L().Debug("Received a non-zero length PAT token")
ctx := context.Background()
dao := apiHandler.AppDao()
@@ -40,7 +40,7 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
}
telemetry.GetInstance().SetPatTokenUser()
dao.UpdatePATLastUsed(ctx, patToken, time.Now().Unix())
user.User.GroupId = group.Id
user.User.GroupId = group.ID
user.User.Id = pat.Id
return &basemodel.UserPayload{
User: user.User,
@@ -52,5 +52,5 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
return nil, err
}
}
return baseauth.GetUserFromRequest(r)
return baseauth.GetUserFromReqContext(ctx)
}

View File

@@ -1,10 +1,10 @@
package dao
import (
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/ee/query-service/dao/sqlite"
"go.signoz.io/signoz/pkg/sqlstore"
)
func InitDao(inputDB *sqlx.DB) (ModelDao, error) {
return sqlite.InitDB(inputDB)
func InitDao(sqlStore sqlstore.SQLStore) (ModelDao, error) {
return sqlite.InitDB(sqlStore)
}

View File

@@ -10,6 +10,7 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types/authtypes"
)
type ModelDao interface {
@@ -22,7 +23,7 @@ type ModelDao interface {
// auth methods
CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError)
PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError)
PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError)
GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error)
// org domain (auth domains) CRUD ops

View File

@@ -14,6 +14,7 @@ import (
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/query-service/utils"
"go.signoz.io/signoz/pkg/types/authtypes"
"go.uber.org/zap"
)
@@ -48,7 +49,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
Password: hash,
CreatedAt: time.Now().Unix(),
ProfilePictureURL: "", // Currently unused
GroupId: group.Id,
GroupId: group.ID,
OrgId: domain.OrgId,
}
@@ -64,7 +65,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
// PrepareSsoRedirect prepares redirect page link after SSO response
// is successfully parsed (i.e. valid email is available)
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) {
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError) {
userPayload, apierr := m.GetUserByEmail(ctx, email)
if !apierr.IsNil() {
@@ -85,7 +86,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
user = &userPayload.User
}
tokenStore, err := baseauth.GenerateJWTForUser(user)
tokenStore, err := baseauth.GenerateJWTForUser(user, jwt)
if err != nil {
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
return "", model.InternalErrorStr("failed to generate token for the user")

View File

@@ -7,6 +7,7 @@ import (
basedao "go.signoz.io/signoz/pkg/query-service/dao"
basedsql "go.signoz.io/signoz/pkg/query-service/dao/sqlite"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/sqlstore"
)
type modelDao struct {
@@ -29,8 +30,8 @@ func (m *modelDao) checkFeature(key string) error {
}
// InitDB creates and extends base model DB repository
func InitDB(inputDB *sqlx.DB) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB)
func InitDB(sqlStore sqlstore.SQLStore) (*modelDao, error) {
dao, err := basedsql.InitDB(sqlStore)
if err != nil {
return nil, err
}

View File

@@ -9,21 +9,25 @@ import (
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"github.com/uptrace/bun"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.signoz.io/signoz/pkg/types"
"go.uber.org/zap"
)
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
db *sqlx.DB
bundb *bun.DB
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB) Repo {
func NewLicenseRepo(db *sqlx.DB, bundb *bun.DB) Repo {
return Repo{
db: db,
db: db,
bundb: bundb,
}
}
@@ -165,24 +169,25 @@ func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
return nil
}
func (r *Repo) CreateFeature(req *basemodel.Feature) *basemodel.ApiError {
func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {
_, err := r.db.Exec(
`INSERT INTO feature_status (name, active, usage, usage_limit, route)
VALUES (?, ?, ?, ?, ?);`,
req.Name, req.Active, req.Usage, req.UsageLimit, req.Route)
_, err := r.bundb.NewInsert().
Model(req).
Exec(context.Background())
if err != nil {
return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
}
return nil
}
func (r *Repo) GetFeature(featureName string) (basemodel.Feature, error) {
func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
var feature types.FeatureStatus
var feature basemodel.Feature
err := r.bundb.NewSelect().
Model(&feature).
Where("name = ?", featureName).
Scan(context.Background())
err := r.db.Get(&feature,
`SELECT * FROM feature_status WHERE name = ?;`, featureName)
if err != nil {
return feature, err
}
@@ -205,18 +210,19 @@ func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
return feature, nil
}
func (r *Repo) UpdateFeature(req basemodel.Feature) error {
func (r *Repo) UpdateFeature(req types.FeatureStatus) error {
_, err := r.db.Exec(
`UPDATE feature_status SET active = ?, usage = ?, usage_limit = ?, route = ? WHERE name = ?;`,
req.Active, req.Usage, req.UsageLimit, req.Route, req.Name)
_, err := r.bundb.NewUpdate().
Model(&req).
Where("name = ?", req.Name).
Exec(context.Background())
if err != nil {
return err
}
return nil
}
func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
for _, feature := range req {
currentFeature, err := r.GetFeature(feature.Name)
@@ -229,7 +235,7 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
} else if err != nil {
return err
}
feature.Usage = currentFeature.Usage
feature.Usage = int(currentFeature.Usage)
if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
feature.Active = false
}

View File

@@ -7,11 +7,13 @@ import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"sync"
"go.signoz.io/signoz/pkg/query-service/auth"
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/types"
"go.signoz.io/signoz/pkg/types/authtypes"
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
"go.signoz.io/signoz/ee/query-service/model"
@@ -43,12 +45,12 @@ type Manager struct {
activeFeatures basemodel.FeatureSet
}
func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(db *sqlx.DB, bundb *bun.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
repo := NewLicenseRepo(db)
repo := NewLicenseRepo(db, bundb)
m := &Manager{
repo: &repo,
}
@@ -237,10 +239,10 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
claims, ok := authtypes.ClaimsFromContext(ctx)
if ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
map[string]interface{}{"err": errResponse.Err.Error()}, claims.Email, true, false)
}
}
}()
@@ -282,15 +284,41 @@ func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
}
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
return lm.repo.InitFeatures(features)
featureStatus := make([]types.FeatureStatus, len(features))
for i, f := range features {
featureStatus[i] = types.FeatureStatus{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return lm.repo.InitFeatures(featureStatus)
}
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
return lm.repo.UpdateFeature(feature)
return lm.repo.UpdateFeature(types.FeatureStatus{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
}
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
return lm.repo.GetFeature(key)
featureStatus, err := lm.repo.GetFeature(key)
if err != nil {
return basemodel.Feature{}, err
}
return basemodel.Feature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
// GetRepo return the license repo

View File

@@ -20,6 +20,7 @@ import (
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/types/authtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -154,6 +155,16 @@ func main() {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
if len(jwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
}
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
serverOptions := &app.ServerOptions{
Config: config,
SigNoz: signoz,
@@ -171,15 +182,7 @@ func main() {
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
}
// Read the jwt secret key
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 {
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.L().Info("JWT secret key set successfully.")
Jwt: jwt,
}
server, err := app.NewServer(serverOptions)

View File

@@ -157,13 +157,6 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -286,13 +279,6 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -429,11 +415,4 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AWSIntegration,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -6,7 +6,9 @@ import loginApi from 'api/user/login';
import afterLogin from 'AppRoutes/utils';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, {
apiAlertManager,
@@ -18,13 +20,40 @@ import apiV1, {
} from './apiV1';
import { Logout } from './utils';
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
const interceptorsResponse = (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => Promise.resolve(value);
): Promise<AxiosResponse<any>> => {
if ((value.config as any)?.metadata) {
const duration =
new Date().getTime() - (value.config as any).metadata.startTime;
if (duration > RESPONSE_TIMEOUT_THRESHOLD && value.config.url !== '/event') {
eventEmitter.emit(Events.SLOW_API_WARNING, true, {
duration,
url: value.config.url,
threshold: RESPONSE_TIMEOUT_THRESHOLD,
});
console.warn(
`[API Warning] Request to ${value.config.url} took ${duration}ms`,
);
}
}
return Promise.resolve(value);
};
const interceptorsRequestResponse = (
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
// Attach metadata safely (not sent with the request)
Object.defineProperty(value, 'metadata', {
value: { startTime: new Date().getTime() },
enumerable: false, // Prevents it from being included in the request
});
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
if (value && value.headers) {

View File

@@ -15,6 +15,7 @@ import {
Typography,
} from 'antd';
import { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import {
getQueueOverview,
QueueOverviewResponse,
@@ -458,6 +459,7 @@ export default function CeleryOverviewTable({
const handleRowClick = (record: RowData): void => {
onRowClick(record);
logEvent('MQ Overview Page: Right Panel', { ...record });
};
const getFilteredData = useCallback(
@@ -481,6 +483,22 @@ export default function CeleryOverviewTable({
tableData,
]);
const prevTableDataRef = useRef<string>();
useEffect(() => {
if (tableData.length > 0) {
const currentTableData = JSON.stringify(tableData);
if (currentTableData !== prevTableDataRef.current) {
logEvent(`MQ Overview Page: List rendered`, {
dataRender: tableData.length,
});
prevTableDataRef.current = currentTableData;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(tableData)]);
return (
<div className="celery-overview-table-container">
<Input.Search

View File

@@ -2,6 +2,7 @@ import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -98,6 +99,12 @@ export default function CeleryTaskDetail({
...rowData,
[taskData.entity]: taskData.value,
});
logEvent('MQ Celery: navigation to trace page', {
filters,
startTime,
endTime,
source: widgetData.title,
});
navigateToTrace(filters, startTime, endTime);
}}
start={startTime}

View File

@@ -42,10 +42,12 @@ import {
function CeleryTaskBar({
onClick,
queryEnabled,
checkIfDataExists,
}: {
onClick?: (task: CaptureDataProps) => void;
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -187,6 +189,7 @@ function CeleryTaskBar({
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
}
customSeries={getCustomSeries}
dataAvailable={checkIfDataExists}
/>
)}
{barState === CeleryTaskState.Failed && (
@@ -232,6 +235,7 @@ function CeleryTaskBar({
CeleryTaskBar.defaultProps = {
onClick: (): void => {},
checkIfDataExists: undefined,
};
export default CeleryTaskBar;

View File

@@ -35,6 +35,8 @@ function CeleryTaskGraph({
customErrorMessage,
start,
end,
checkIfDataExists,
analyticsEvent,
}: {
widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void;
@@ -48,6 +50,8 @@ function CeleryTaskGraph({
customErrorMessage?: string;
start?: number;
end?: number;
checkIfDataExists?: (isDataAvailable: boolean) => void;
analyticsEvent?: string;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -125,6 +129,8 @@ function CeleryTaskGraph({
customErrorMessage={customErrorMessage}
start={start}
end={end}
dataAvailable={checkIfDataExists}
analyticsEvent={analyticsEvent}
/>
</Card>
);
@@ -141,6 +147,8 @@ CeleryTaskGraph.defaultProps = {
customErrorMessage: undefined,
start: undefined,
end: undefined,
checkIfDataExists: undefined,
analyticsEvent: undefined,
};
export default CeleryTaskGraph;

View File

@@ -1,6 +1,7 @@
import './CeleryTaskGraph.style.scss';
import { Card, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
@@ -92,6 +93,15 @@ export default function CeleryTaskGraphGrid({
}));
};
const checkIfDataExists = (isDataAvailable: boolean, title: string): void => {
if (isDataAvailable) {
logEvent(`MQ Celery: ${title} data exists`, {
graph: title,
isDataAvailable,
});
}
};
return (
<div className="celery-task-graph-grid-container">
<div className="metric-based-graphs">
@@ -124,6 +134,10 @@ export default function CeleryTaskGraphGrid({
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
customErrorMessage="Enable Flower metrics to view this graph"
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'Active Tasks by worker')
}
analyticsEvent="MQ Celery: Flower metric not enabled"
/>
<Card className="celery-task-graph-worker-count">
<div className="worker-count-header">
@@ -173,8 +187,19 @@ export default function CeleryTaskGraphGrid({
</div>
{!collapsedSections.traceBasedGraphs && (
<>
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskLatencyGraph queryEnabled={queryEnabled} />
<CeleryTaskBar
queryEnabled={queryEnabled}
onClick={onClick}
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'State Graph')
}
/>
<CeleryTaskLatencyGraph
queryEnabled={queryEnabled}
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, 'Task Latency')
}
/>
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph
@@ -184,6 +209,9 @@ export default function CeleryTaskGraphGrid({
queryEnabled={queryEnabled}
rightPanelTitle={rightPanelTitle[index]}
applyCeleryTaskFilter
checkIfDataExists={(isDataAvailable): void =>
checkIfDataExists(isDataAvailable, rightPanelTitle[index])
}
/>
))}
</div>

View File

@@ -1,6 +1,7 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
@@ -40,8 +41,10 @@ export enum CeleryTaskGraphState {
function CeleryTaskLatencyGraph({
queryEnabled,
checkIfDataExists,
}: {
queryEnabled: boolean;
checkIfDataExists?: (isDataAvailable: boolean) => void;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
@@ -61,6 +64,10 @@ function CeleryTaskLatencyGraph({
const handleTabClick = (key: CeleryTaskGraphState): void => {
setGraphState(key as CeleryTaskGraphState);
logEvent('MQ Celery: Task latency graph tab clicked', {
taskName: urlQuery.get(QueryParams.taskName),
graphState: key,
});
};
const onDragSelect = useCallback(
@@ -195,6 +202,7 @@ function CeleryTaskLatencyGraph({
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p99_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
)}
@@ -215,6 +223,7 @@ function CeleryTaskLatencyGraph({
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p95_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
)}
@@ -234,6 +243,7 @@ function CeleryTaskLatencyGraph({
onDragSelect={onDragSelect}
onClickHandler={onGraphClick('Celery_p90_latency')}
isQueryEnabled={queryEnabled}
dataAvailable={checkIfDataExists}
/>
</>
)}
@@ -243,3 +253,7 @@ function CeleryTaskLatencyGraph({
}
export default CeleryTaskLatencyGraph;
CeleryTaskLatencyGraph.defaultProps = {
checkIfDataExists: undefined,
};

View File

@@ -2,6 +2,7 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Dispatch, SetStateAction, useMemo } from 'react';
@@ -44,12 +45,16 @@ function CeleryTaskStateGraphConfig({
{ label: 'Successful', key: CeleryTaskState.Successful },
];
const urlQuery = useUrlQuery();
const handleTabClick = (key: CeleryTaskState): void => {
setBarState(key as CeleryTaskState);
logEvent('MQ Celery: State graph tab clicked', {
taskName: urlQuery.get(QueryParams.taskName),
graphState: key,
});
};
const urlQuery = useUrlQuery();
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(

View File

@@ -18,7 +18,9 @@ import {
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
@@ -29,6 +31,7 @@ import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
@@ -83,7 +86,20 @@ function ExplorerCard({
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const { options } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),

View File

@@ -6,11 +6,24 @@ import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = 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}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({

View File

@@ -2,6 +2,7 @@ import { FormInstance } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { UseMutateAsyncFunction } from 'react-query';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -36,6 +37,7 @@ export interface IsQueryUpdatedInViewProps {
data: ViewProps[] | undefined;
stagedQuery: Query | null;
currentPanelType: PANEL_TYPES | null;
options: OptionsQuery;
}
export interface SaveViewWithNameProps {

View File

@@ -80,12 +80,13 @@ export const isQueryUpdatedInView = ({
data,
stagedQuery,
currentPanelType,
options,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType } = currentViewDetails;
const { query, panelType, extraData } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
@@ -97,12 +98,15 @@ export const isQueryUpdatedInView = ({
) {
return false;
}
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
!isEqual(query.promql, updatedCurrentQuery?.promql)
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
)
);
};

View File

@@ -67,6 +67,10 @@ function HostMetricTraces({
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
op: 'AND',
},
},
],
},

View File

@@ -50,6 +50,10 @@ function HostMetricLogsDetailedView({
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters: {
items: [],
op: 'AND',
},
},
],
},

View File

@@ -6,6 +6,8 @@ import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -25,8 +27,12 @@ function DynamicColumnTable({
onDragColumn,
facingIssueBtn,
shouldSendAlertsLogEvent,
pagination,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
columns,
);
@@ -93,6 +99,28 @@ function DynamicColumnTable({
type: 'checkbox',
})) || [];
// Get current page from URL or default to 1
const currentPage = Number(urlQuery.get('page')) || 1;
const handlePaginationChange = (page: number, pageSize?: number): void => {
// Update URL with new page number while preserving other params
urlQuery.set('page', page.toString());
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
safeNavigate(newUrl);
// Call original pagination handler if provided
if (pagination?.onChange && !!pageSize) {
pagination.onChange(page, pageSize);
}
};
const enhancedPagination = {
...pagination,
current: currentPage, // Ensure the pagination component shows the correct page
onChange: handlePaginationChange,
};
return (
<div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}>
@@ -116,6 +144,7 @@ function DynamicColumnTable({
<ResizeTable
columns={columnsData}
onDragColumn={onDragColumn}
pagination={enhancedPagination}
{...restProps}
/>
</div>

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { PaginationProps } from 'antd/lib';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
@@ -15,6 +16,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: LaunchChatSupportProps;
shouldSendAlertsLogEvent?: boolean;
pagination?: PaginationProps;
}
export type GetVisibleColumnsFunction = (

View File

@@ -1,31 +0,0 @@
import { Alert, Space } from 'antd';
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
type UpgradePromptProps = {
title?: string;
};
function UpgradePrompt({ title }: UpgradePromptProps): JSX.Element {
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert
message={title}
description={
<div>
This feature is available for paid plans only.{' '}
<a href={SIGNOZ_UPGRADE_PLAN_URL} target="_blank" rel="noreferrer">
Click here
</a>{' '}
to Upgrade
</div>
}
type="warning"
/>{' '}
</Space>
);
}
UpgradePrompt.defaultProps = {
title: 'Upgrade to a Paid Plan',
};
export default UpgradePrompt;

View File

@@ -2,4 +2,5 @@ export enum Events {
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
SLOW_API_WARNING = 'SLOW_API_WARNING',
}

View File

@@ -23,5 +23,4 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
}

View File

@@ -25,4 +25,5 @@ export enum LOCALSTORAGE {
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
}

View File

@@ -378,9 +378,7 @@ describe('Create Alert Channel', () => {
});
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
expect(
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
).toBeInTheDocument();
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly ', () => {

View File

@@ -308,10 +308,8 @@ describe('Create Alert Channel (Normal User)', () => {
render(<CreateAlertChannels preType={ChannelType.MsTeams} />);
});
it('Should check if the selected item in the type dropdown has text "Microsoft Teams (Supported in Paid Plans Only)"', () => {
expect(
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
).toBeInTheDocument();
it('Should check if the selected item in the type dropdown has text "Microsoft Teams"', () => {
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
});
// TODO[vikrantgupta25]: check with Shaheer

View File

@@ -101,13 +101,32 @@
}
}
.trial-expiry-banner {
.trial-expiry-banner,
.slow-api-warning-banner {
padding: 8px;
background-color: #f25733;
color: white;
text-align: center;
}
.slow-api-warning-banner {
background-color: var(--bg-robin-500);
.dismiss-banner {
color: white;
float: right;
padding: 0 16px;
display: flex;
align-items: center;
gap: 4px;
&:hover {
color: white;
}
}
}
.payment-failed-banner {
padding: 8px;
background-color: var(--bg-sakura-500);

View File

@@ -6,13 +6,18 @@ import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Flex } from 'antd';
import manageCreditCardApi from 'api/billing/manage';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
@@ -24,7 +29,14 @@ import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query';
@@ -41,7 +53,9 @@ import {
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { isCloudUser } from 'utils/app';
import { eventEmitter } from 'utils/getEventEmitter';
import {
getFormattedDate,
getFormattedDateWithMinutes,
@@ -72,6 +86,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
setShowPaymentFailedWarning,
] = useState<boolean>(false);
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
@@ -261,22 +278,25 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (!isLoggedIn) {
setShowTrialExpiryBanner(false);
setShowPaymentFailedWarning(false);
setShowSlowApiWarning(false);
}
}, [isLoggedIn]);
const handleUpgrade = (): void => {
if (user.role === 'ADMIN') {
const handleUpgrade = useCallback((): void => {
if (user.role === USER_ROLES.ADMIN) {
history.push(ROUTES.BILLING);
}
};
}, [user.role]);
const handleFailedPayment = (): void => {
manageCreditCard({
licenseKey: activeLicenseV3?.key || '',
successURL: window.location.origin,
cancelURL: window.location.origin,
});
};
const handleFailedPayment = useCallback((): void => {
if (activeLicenseV3?.key) {
manageCreditCard({
licenseKey: activeLicenseV3?.key || '',
successURL: window.location.origin,
cancelURL: window.location.origin,
});
}
}, [activeLicenseV3?.key, manageCreditCard]);
const isLogsView = (): boolean =>
routeKey === 'LOGS' ||
@@ -360,6 +380,107 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
licenses,
]);
// Listen for API warnings
const handleWarning = (
isSlow: boolean,
data: { duration: number; url: string; threshold: number },
): void => {
const dontShowSlowApiWarning = getLocalStorageApi(
LOCALSTORAGE.DONT_SHOW_SLOW_API_WARNING,
);
logEvent(`Slow API Warning`, {
duration: `${data.duration}ms`,
url: data.url,
threshold: data.threshold,
});
const isDontShowSlowApiWarning = dontShowSlowApiWarning === 'true';
if (isDontShowSlowApiWarning) {
setShowSlowApiWarning(false);
} else {
setShowSlowApiWarning(isSlow);
}
};
useEffect(() => {
eventEmitter.on(Events.SLOW_API_WARNING, handleWarning);
return (): void => {
eventEmitter.off(Events.SLOW_API_WARNING, handleWarning);
};
}, []);
const isTrialUser = useMemo(
(): boolean =>
(!isFetchingLicenses &&
licenses &&
licenses.onTrial &&
!licenses.trialConvertedToSubscription) ||
false,
[licenses, isFetchingLicenses],
);
const handleDismissSlowApiWarning = (): void => {
setShowSlowApiWarning(false);
setLocalStorageApi(LOCALSTORAGE.DONT_SHOW_SLOW_API_WARNING, 'true');
};
useEffect(() => {
if (
showSlowApiWarning &&
isTrialUser &&
!licenses?.trialConvertedToSubscription &&
!slowApiWarningShown
) {
setSlowApiWarningShown(true);
notifications.info({
message: (
<div>
Our systems are taking longer than expected for your trial workspace.
Please{' '}
{user.role === USER_ROLES.ADMIN ? (
<span>
<a
className="upgrade-link"
onClick={(): void => {
notifications.destroy('slow-api-warning');
logEvent(`Slow API Banner: Upgrade clicked`, {});
handleUpgrade();
}}
>
upgrade
</a>
your workspace for a smoother experience.
</span>
) : (
'contact your administrator for upgrading to a paid plan for a smoother experience.'
)}
</div>
),
duration: 60000,
placement: 'topRight',
onClose: handleDismissSlowApiWarning,
key: 'slow-api-warning',
});
}
}, [
showSlowApiWarning,
notifications,
isTrialUser,
licenses?.trialConvertedToSubscription,
user.role,
isLoadingManageBilling,
handleFailedPayment,
slowApiWarningShown,
handleUpgrade,
]);
return (
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
<Helmet>
@@ -370,7 +491,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
{user.role === 'ADMIN' ? (
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
Please{' '}
@@ -384,6 +505,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
</div>
)}
{!showTrialExpiryBanner && showPaymentFailedWarning && (
<div className="payment-failed-banner">
Your bill payment has failed. Your workspace will get suspended on{' '}
@@ -393,18 +515,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
.
</span>
{user.role === 'ADMIN' ? (
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
Please{' '}
<a
className="upgrade-link"
onClick={(): void => {
if (!isLoadingManageBilling) {
handleFailedPayment();
}
}}
>
<a className="upgrade-link" onClick={handleFailedPayment}>
pay the bill
</a>
to continue using SigNoz features.

View File

@@ -3,7 +3,7 @@ import './AccountActions.style.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import { useAwsAccounts } from 'hooks/integrations/aws/useAwsAccounts';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, ChevronDown } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
@@ -159,8 +159,9 @@ function AccountActions(): JSX.Element {
useEffect(() => {
if (initialAccount !== null) {
setActiveAccount(initialAccount);
urlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: urlQuery.toString() });
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('cloudAccountId', initialAccount.cloud_account_id);
navigate({ search: latestUrlQuery.toString() });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialAccount]);

View File

@@ -6,7 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
getRegionPreviewText,
useAccountSettingsModal,
} from 'hooks/integrations/aws/useAccountSettingsModal';
} from 'hooks/integration/aws/useAccountSettingsModal';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { Dispatch, SetStateAction, useCallback } from 'react';

View File

@@ -2,9 +2,11 @@ import './CloudAccountSetupModal.style.scss';
import { Color } from '@signozhq/design-tokens';
import SignozModal from 'components/SignozModal/SignozModal';
import { useIntegrationModal } from 'hooks/integrations/aws/useIntegrationModal';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useIntegrationModal } from 'hooks/integration/aws/useIntegrationModal';
import { SquareArrowOutUpRight } from 'lucide-react';
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import {
ActiveViewEnum,
@@ -18,6 +20,7 @@ import { SuccessView } from './SuccessView';
function CloudAccountSetupModal({
onClose,
}: IntegrationModalProps): JSX.Element {
const queryClient = useQueryClient();
const {
form,
modalState,
@@ -110,7 +113,10 @@ function CloudAccountSetupModal({
</div>
),
block: true,
onOk: handleClose,
onOk: (): void => {
queryClient.invalidateQueries([REACT_QUERY_KEY.AWS_ACCOUNTS]);
handleClose();
},
cancelButtonProps: { style: { display: 'none' } },
disabled: false,
};
@@ -151,6 +157,7 @@ function CloudAccountSetupModal({
activeView,
handleClose,
setActiveView,
queryClient,
]);
const modalConfig = getModalConfig();

View File

@@ -1,6 +1,6 @@
import { Form } from 'antd';
import cx from 'classnames';
import { useAccountStatus } from 'hooks/integrations/aws/useAccountStatus';
import { useAccountStatus } from 'hooks/integration/aws/useAccountStatus';
import { useRef } from 'react';
import { AccountStatusResponse } from 'types/api/integrations/aws';
import { regions } from 'utils/regions';

View File

@@ -1,7 +1,7 @@
import './RegionSelector.style.scss';
import { Checkbox } from 'antd';
import { useRegionSelection } from 'hooks/integrations/aws/useRegionSelection';
import { useRegionSelection } from 'hooks/integration/aws/useRegionSelection';
import { Dispatch, SetStateAction } from 'react';
import { regions } from 'utils/regions';

View File

@@ -7,7 +7,7 @@ import {
ServiceConfig,
SupportedSignals,
} from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateServiceConfig } from 'hooks/integrations/aws/useUpdateServiceConfig';
import { useUpdateServiceConfig } from 'hooks/integration/aws/useUpdateServiceConfig';
import { useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
@@ -34,10 +34,19 @@ function ConfigureServiceModal({
const [isLoading, setIsLoading] = useState(false);
// Track current form values
const [currentValues, setCurrentValues] = useState({
const initialValues = {
metrics: initialConfig?.metrics?.enabled || false,
logs: initialConfig?.logs?.enabled || false,
});
};
const [currentValues, setCurrentValues] = useState(initialValues);
const isSaveDisabled = useMemo(
() =>
// disable only if current values are same as the initial config
currentValues.metrics === initialValues.metrics &&
currentValues.logs === initialValues.logs,
[currentValues, initialValues.metrics, initialValues.logs],
);
const {
mutate: updateServiceConfig,
@@ -93,11 +102,6 @@ function ConfigureServiceModal({
onClose,
]);
const isSaveDisabled = useMemo(
() => currentValues.metrics === false && currentValues.logs === false,
[currentValues],
);
const handleClose = useCallback(() => {
form.resetFields();
onClose();

View File

@@ -5,7 +5,7 @@ import CloudServiceDashboards from 'container/CloudIntegrationPage/ServicesSecti
import CloudServiceDataCollected from 'container/CloudIntegrationPage/ServicesSection/CloudServiceDataCollected';
import { IServiceStatus } from 'container/CloudIntegrationPage/ServicesSection/types';
import dayjs from 'dayjs';
import { useServiceDetails } from 'hooks/integrations/aws/useServiceDetails';
import { useServiceDetails } from 'hooks/integration/aws/useServiceDetails';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo, useState } from 'react';
@@ -111,24 +111,27 @@ function ServiceDetails(): JSX.Element | null {
<div className="service-details__title-bar">
<div className="service-details__details-title">Details</div>
<div className="service-details__right-actions">
<ServiceStatus serviceStatus={serviceDetailsData.status} />
{!!cloudAccountId && isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
{isAnySignalConfigured && (
<ServiceStatus serviceStatus={serviceDetailsData.status} />
)}
{!!cloudAccountId &&
(isAnySignalConfigured ? (
<Button
className="configure-button configure-button--default"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Configure ({enabledSignals}/{totalSupportedSignals})
</Button>
) : (
<Button
type="primary"
className="configure-button configure-button--primary"
onClick={(): void => setIsConfigureServiceModalOpen(true)}
>
Enable Service
</Button>
))}
</div>
</div>
<div className="service-details__overview">

View File

@@ -1,5 +1,5 @@
import Spinner from 'components/Spinner';
import { useGetAccountServices } from 'hooks/integrations/aws/useGetAccountServices';
import { useGetAccountServices } from 'hooks/integration/aws/useGetAccountServices';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
@@ -24,10 +24,11 @@ function ServicesList({
const handleActiveService = useCallback(
(serviceId: string): void => {
urlQuery.set('service', serviceId);
navigate({ search: urlQuery.toString() });
const latestUrlQuery = new URLSearchParams(window.location.search);
latestUrlQuery.set('service', serviceId);
navigate({ search: latestUrlQuery.toString() });
},
[navigate, urlQuery],
[navigate],
);
const filteredServices = useMemo(() => {

View File

@@ -3,7 +3,7 @@ import './ServicesTabs.style.scss';
import { Color } from '@signozhq/design-tokens';
import type { SelectProps, TabsProps } from 'antd';
import { Select, Tabs } from 'antd';
import { getAwsServices } from 'api/integrations/aws';
import { getAwsServices } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useUrlQuery from 'hooks/useUrlQuery';
import { ChevronDown } from 'lucide-react';

View File

@@ -27,6 +27,12 @@ jest.mock('uplot', () => {
};
});
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
let mockWindowOpen: jest.Mock;
window.ResizeObserver =

View File

@@ -36,6 +36,11 @@ window.ResizeObserver =
unobserve: jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Anomaly Alert Documentation Redirection', () => {
let mockWindowOpen: jest.Mock;

View File

@@ -212,13 +212,46 @@ function ExplorerOptions({
0.08,
);
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
): string => {
let updatedExtraData;
if (extraData) {
const parsedExtraData = JSON.parse(extraData);
parsedExtraData.selectColumns = newSelectedColumns;
updatedExtraData = JSON.stringify(parsedExtraData);
} else {
updatedExtraData = JSON.stringify({
color: Color.BG_SIENNA_500,
selectColumns: newSelectedColumns,
});
}
return updatedExtraData;
};
const updatedExtraData = getUpdatedExtraData(
extraData,
options?.selectColumns,
);
const {
mutateAsync: updateViewAsync,
isLoading: isViewUpdating,
} = useUpdateView({
compositeQuery,
viewKey,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
extraData: updatedExtraData,
sourcePage: sourcepage,
viewName,
});
@@ -230,13 +263,11 @@ function ExplorerOptions({
};
const onUpdateQueryHandler = (): void => {
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
?.extraData;
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
extraData: updatedExtraData,
sourcePage: sourcepage,
viewName,
},
@@ -258,15 +289,6 @@ function ExplorerOptions({
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { options, handleOptionsChange } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
type ExtraData = {
selectColumns?: BaseAutocompleteData[];
version?: number;
@@ -422,7 +444,11 @@ function ExplorerOptions({
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const {
isLoading: isSaveViewLoading,

View File

@@ -1,6 +1,5 @@
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
import { Store } from 'antd/lib/form/interface';
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import {
@@ -15,6 +14,7 @@ import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
import { isFeatureKeys } from 'utils/app';
import EmailSettings from './Settings/Email';
@@ -40,32 +40,18 @@ function FormAlertChannels({
}: FormAlertChannelsProps): JSX.Element {
const { t } = useTranslation('channels');
const { featureFlags } = useAppContext();
const isUserOnEEPlan =
featureFlags?.find((flag) => flag.name === FeatureKeys.ENTERPRISE_PLAN)
?.active || false;
const feature = `ALERT_CHANNEL_${type.toUpperCase()}`;
const featureKey = isFeatureKeys(feature)
? feature
: FeatureKeys.ALERT_CHANNEL_SLACK;
const hasFeature = featureFlags?.find((flag) => flag.name === featureKey);
const isOssFeature = featureFlags?.find(
(flag) => flag.name === FeatureKeys.OSS,
const hasFeature = featureFlags?.find(
(flag: FeatureFlagProps) => flag.name === featureKey,
);
const renderSettings = (): ReactElement | null => {
if (
// for ee plan
!isOssFeature?.active &&
(!hasFeature || !hasFeature.active) &&
type === 'msteams'
) {
// channel type is not available for users plan
return <UpgradePrompt />;
}
switch (type) {
case ChannelType.Slack:
return <SlackSettings setSelectedConfig={setSelectedConfig} />;
@@ -149,13 +135,10 @@ function FormAlertChannels({
<Select.Option value="email" key="email" data-testid="select-option">
Email
</Select.Option>
{!isOssFeature?.active && (
<Select.Option value="msteams" key="msteams" data-testid="select-option">
<div>
Microsoft Teams {!isUserOnEEPlan && '(Supported in Paid Plans Only)'}{' '}
</div>
</Select.Option>
)}
<Select.Option value="msteams" key="msteams" data-testid="select-option">
Microsoft Teams
</Select.Option>
</Select>
</Form.Item>

View File

@@ -17,8 +17,8 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
@@ -87,7 +87,7 @@ function FormAlertRules({
// init namespace for translations
const { t } = useTranslation('alerts');
const { featureFlags } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -224,7 +224,7 @@ function FormAlertRules({
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod]);
@@ -295,8 +295,8 @@ function FormAlertRules({
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [urlQuery]);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [safeNavigate, urlQuery]);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
@@ -515,7 +515,7 @@ function FormAlertRules({
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} else {
logData = {

View File

@@ -20,11 +20,11 @@ import {
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useChartMutable } from 'hooks/useChartMutable';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -49,6 +49,7 @@ function FullView({
isDependedDataLoaded = false,
onToggleModelHandler,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
@@ -137,9 +138,9 @@ function FullView({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
safeNavigate(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
[dispatch, location.pathname, safeNavigate, urlQuery],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<

View File

@@ -23,6 +23,12 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),

View File

@@ -10,9 +10,9 @@ import { placeWidgetAtBottom } from 'container/NewWidget/utils';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -51,6 +51,7 @@ function WidgetGraphComponent({
customSeries,
customErrorMessage,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
const { notifications } = useNotifications();
@@ -173,7 +174,7 @@ function WidgetGraphComponent({
graphType: widget?.panelTypes,
widgetId: uuid,
};
history.push(`${pathname}/new?${createQueryParams(queryParams)}`);
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
},
},
);
@@ -194,7 +195,7 @@ function WidgetGraphComponent({
const separator = existingSearch.toString() ? '&' : '';
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
history.push({
safeNavigate({
pathname,
search: newSearch,
});
@@ -221,7 +222,7 @@ function WidgetGraphComponent({
});
setGraphVisibility(localStoredVisibilityState);
}
history.push({
safeNavigate({
pathname,
search: createQueryParams(updatedQueryParams),
});

View File

@@ -1,3 +1,4 @@
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -44,6 +45,7 @@ function GridCardGraph({
customErrorMessage,
start,
end,
analyticsEvent,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@@ -219,6 +221,11 @@ function GridCardGraph({
setIsInternalServerError(
String(error.message).includes('API responded with 500'),
);
if (analyticsEvent) {
logEvent(analyticsEvent, {
error: error.message,
});
}
}
setDashboardQueryRangeCalled(true);
},
@@ -283,6 +290,7 @@ GridCardGraph.defaultProps = {
threshold: undefined,
headerMenuList: [MenuItemKeys.View],
version: 'v3',
analyticsEvent: undefined,
};
export default memo(GridCardGraph);

View File

@@ -58,6 +58,7 @@ export interface GridCardGraphProps {
customErrorMessage?: string;
start?: number;
end?: number;
analyticsEvent?: string;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -15,8 +15,8 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import {
@@ -55,6 +55,7 @@ interface GraphLayoutProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { handle } = props;
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
layouts,
@@ -66,6 +67,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
} = useDashboard();
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
@@ -214,13 +216,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
safeNavigate(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
[dispatch, pathname, safeNavigate, urlQuery],
);
useEffect(() => {
@@ -231,7 +233,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
!isEqual(layouts, dashboardLayout) &&
!isDashboardLocked &&
saveLayoutPermission &&
!updateDashboardMutation.isLoading
!updateDashboardMutation.isLoading &&
!isDashboardFetching
) {
onSaveHandler();
}

View File

@@ -18,8 +18,8 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { CircleX, X } from 'lucide-react';
@@ -72,6 +72,7 @@ function WidgetHeader({
setSearchTerm,
}: IWidgetHeaderProps): JSX.Element | null {
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
urlQuery.set(QueryParams.widgetId, widgetId);
@@ -81,8 +82,8 @@ function WidgetHeader({
encodeURIComponent(JSON.stringify(widget.query)),
);
const generatedUrl = `${window.location.pathname}/new?${urlQuery}`;
history.push(generatedUrl);
}, [urlQuery, widget.id, widget.panelTypes, widget.query]);
safeNavigate(generatedUrl);
}, [safeNavigate, urlQuery, widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');

View File

@@ -55,11 +55,14 @@
.hosts-list-content {
display: flex;
flex-direction: row;
align-items: stretch;
height: calc(100vh - 110px);
.hosts-quick-filters-container {
width: 280px;
min-width: 280px;
border-right: 1px solid var(--bg-slate-400);
height: 100%;
.hosts-quick-filters-container-header {
padding: 8px;
@@ -193,7 +196,7 @@
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
width: calc(100% - 364px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;

View File

@@ -35,7 +35,7 @@ import dayjs from 'dayjs';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { get, isEmpty, isUndefined } from 'lodash-es';
import {
ArrowDownWideNarrow,
@@ -74,7 +74,7 @@ import {
} from 'react';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { generatePath, Link } from 'react-router-dom';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import {
Dashboard,
@@ -105,7 +105,7 @@ function DashboardsList(): JSX.Element {
} = useGetAllDashboard();
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const {
listSortOrder: sortOrder,
setListSortOrder: setSortOrder,
@@ -293,7 +293,7 @@ function DashboardsList(): JSX.Element {
});
if (response.statusCode === 200) {
history.push(
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),
@@ -313,7 +313,7 @@ function DashboardsList(): JSX.Element {
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, t]);
}, [newDashboardState, safeNavigate, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
@@ -418,7 +418,7 @@ function DashboardsList(): JSX.Element {
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
history.push(getLink());
safeNavigate(getLink());
}
logEvent('Dashboard List: Clicked on dashboard', {
dashboardId: dashboard.id,
@@ -444,10 +444,12 @@ function DashboardsList(): JSX.Element {
placement="left"
overlayClassName="title-toolip"
>
<Link
to={getLink()}
<div
className="title-link"
onClick={(e): void => e.stopPropagation()}
onClick={(e): void => {
e.stopPropagation();
safeNavigate(getLink());
}}
>
<img
src={dashboard?.image || Base64Icons[0]}
@@ -460,7 +462,7 @@ function DashboardsList(): JSX.Element {
>
{dashboard.name}
</Typography.Text>
</Link>
</div>
</Tooltip>
</div>

View File

@@ -18,8 +18,8 @@ import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
// See more: https://github.com/lucide-icons/lucide/issues/94
@@ -33,6 +33,7 @@ function ImportJSON({
uploadedGrafana,
onModalHandler,
}: ImportJSONProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [jsonData, setJsonData] = useState<Record<string, unknown>>();
const { t } = useTranslation(['dashboard', 'common']);
const [isUploadJSONError, setIsUploadJSONError] = useState<boolean>(false);
@@ -97,7 +98,7 @@ function ImportJSON({
});
if (response.statusCode === 200) {
history.push(
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),

View File

@@ -2,14 +2,12 @@ import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import { themeColors } from 'constants/theme';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getChartData, { GetChartDataProps } from 'lib/getChartData';
import GetMinMax from 'lib/getMinMax';
import { colors } from 'lib/getRandomColor';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
@@ -28,6 +26,7 @@ function LogsExplorerChart({
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = useCallback(
(element, index, allLabels) => ({
data: element,
@@ -62,41 +61,13 @@ function LogsExplorerChart({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
safeNavigate(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
[dispatch, location.pathname, safeNavigate, urlQuery],
);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
const endTime = searchParams.get(QueryParams.endTime);
const relativeTime = searchParams.get(
QueryParams.relativeTime,
) as CustomTimeType;
if (relativeTime) {
dispatch(UpdateTimeInterval(relativeTime));
} else if (startTime && endTime && startTime !== endTime) {
dispatch(
UpdateTimeInterval('custom', [
parseInt(getTimeString(startTime), 10),
parseInt(getTimeString(endTime), 10),
]),
);
}
};
useEffect(() => {
window.addEventListener('popstate', handleBackNavigation);
return (): void => {
window.removeEventListener('popstate', handleBackNavigation);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const graphData = useMemo(
() =>
getChartData({

View File

@@ -38,6 +38,7 @@ import useAxiosError from 'hooks/useAxiosError';
import useClickOutside from 'hooks/useClickOutside';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { FlatLogData } from 'lib/logs/flatLogData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
@@ -62,7 +63,6 @@ import {
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
@@ -98,7 +98,7 @@ function LogsExplorerViews({
chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
const { safeNavigate } = useSafeNavigate();
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@@ -486,7 +486,7 @@ function LogsExplorerViews({
widgetId,
});
history.push(dashboardEditView);
safeNavigate(dashboardEditView);
},
onError: handleAxisError,
});
@@ -495,7 +495,7 @@ function LogsExplorerViews({
getUpdatedQueryForExport,
exportDefaultQuery,
options.selectColumns,
history,
safeNavigate,
notifications,
panelType,
updateDashboard,

View File

@@ -75,6 +75,12 @@ jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
useGetExplorerQueryRange: jest.fn(),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
beforeEach(() => {
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({

View File

@@ -13,6 +13,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
@@ -157,6 +158,7 @@ function DBCall(): JSX.Element {
servicename,
isDBCall: true,
});
const { safeNavigate } = useSafeNavigate();
return (
<Row gutter={24}>
@@ -171,6 +173,7 @@ function DBCall(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -206,6 +209,7 @@ function DBCall(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces

View File

@@ -15,6 +15,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
@@ -220,6 +221,8 @@ function External(): JSX.Element {
isExternalCall: true,
});
const { safeNavigate } = useSafeNavigate();
return (
<>
<Row gutter={24}>
@@ -234,6 +237,7 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery: errorApmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -270,6 +274,7 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -309,6 +314,7 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -345,6 +351,7 @@ function External(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces

View File

@@ -13,6 +13,7 @@ import {
convertRawQueriesToTraceSelectedTags,
resourceAttributesToTagFilterItems,
} from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import history from 'lib/history';
@@ -29,6 +30,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
@@ -219,14 +221,21 @@ function Application(): JSX.Element {
[dispatch, pathname, urlQuery],
);
/**
*
* @param timestamp - The timestamp in seconds
* @param apmToTraceQuery - query object
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
* @returns A callback function that handles the navigation when executed
*/
const onErrorTrackHandler = useCallback(
(
timestamp: number,
apmToTraceQuery: Query,
isViewLogsClicked?: boolean,
): (() => void) => (): void => {
const endTime = timestamp;
const startTime = timestamp - stepInterval;
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - stepInterval);
const urlParams = new URLSearchParams(search);
urlParams.set(QueryParams.startTime, startTime.toString());
@@ -290,6 +299,7 @@ function Application(): JSX.Element {
},
],
});
const { safeNavigate } = useSafeNavigate();
return (
<>
@@ -317,6 +327,7 @@ function Application(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces
@@ -346,6 +357,7 @@ function Application(): JSX.Element {
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
>
View Traces

View File

@@ -12,6 +12,7 @@ import { latency } from 'container/MetricsApplication/MetricsPageQueries/Overvie
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { useAppContext } from 'providers/App/App';
import { useMemo } from 'react';
@@ -85,6 +86,8 @@ function ServiceOverview({
const apmToLogQuery = useGetAPMToLogsQueries({ servicename });
const { safeNavigate } = useSafeNavigate();
return (
<>
<GraphControlsPanel
@@ -96,6 +99,7 @@ function ServiceOverview({
apmToTraceQuery: apmToLogQuery,
isViewLogsClicked: true,
stepInterval,
safeNavigate,
})}
onViewTracesClick={onViewTracePopupClick({
servicename,
@@ -103,6 +107,7 @@ function ServiceOverview({
timestamp: selectedTimeStamp,
apmToTraceQuery,
stepInterval,
safeNavigate,
})}
/>
<Card data-testid="service_latency">

View File

@@ -1,5 +1,6 @@
import { Tooltip, Typography } from 'antd';
import { navigateToTrace } from 'container/MetricsApplication/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { v4 as uuid } from 'uuid';
@@ -14,6 +15,7 @@ function ColumnWithLink({
record,
}: LinkColumnProps): JSX.Element {
const text = record.toString();
const { safeNavigate } = useSafeNavigate();
const apmToTraceQuery = useGetAPMToTracesQueries({
servicename,
@@ -42,6 +44,7 @@ function ColumnWithLink({
maxTime,
selectedTraceTags,
apmToTraceQuery,
safeNavigate,
});
};

View File

@@ -6,7 +6,6 @@ import { getQueryString } from 'container/SideNav/helper';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
import history from 'lib/history';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import { Dispatch, SetStateAction, useMemo } from 'react';
@@ -17,6 +16,7 @@ import {
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
export const dbSystemTags: Tags[] = [
@@ -36,6 +36,7 @@ interface OnViewTracePopupClickProps {
apmToTraceQuery: Query;
isViewLogsClicked?: boolean;
stepInterval?: number;
safeNavigate: (url: string) => void;
}
export function generateExplorerPath(
@@ -56,6 +57,18 @@ export function generateExplorerPath(
}
// TODO(@rahul-signoz): update the name of this function once we have view logs button in every panel
/**
* Handles click events for viewing trace/logs popup
* @param selectedTraceTags - Selected trace tags
* @param servicename - Name of the service
* @param timestamp - Timestamp in seconds
* @param apmToTraceQuery - Query object
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
* @param stepInterval - Time interval in seconds
* @param safeNavigate - Navigation function
*/
export function onViewTracePopupClick({
selectedTraceTags,
servicename,
@@ -63,10 +76,11 @@ export function onViewTracePopupClick({
apmToTraceQuery,
isViewLogsClicked,
stepInterval,
safeNavigate,
}: OnViewTracePopupClickProps): VoidFunction {
return (): void => {
const endTime = timestamp;
const startTime = timestamp - (stepInterval || 60);
const endTime = secondsToMilliseconds(timestamp);
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
const urlParams = new URLSearchParams(window.location.search);
urlParams.set(QueryParams.startTime, startTime.toString());
@@ -88,7 +102,7 @@ export function onViewTracePopupClick({
queryString,
);
history.push(newPath);
safeNavigate(newPath);
};
}
@@ -111,7 +125,7 @@ export function onGraphClickHandler(
buttonElement.style.display = 'block';
buttonElement.style.left = `${mouseX}px`;
buttonElement.style.top = `${mouseY}px`;
setSelectedTimeStamp(xValue);
setSelectedTimeStamp(Math.floor(xValue));
}
} else if (buttonElement && buttonElement.style.display === 'block') {
buttonElement.style.display = 'none';

View File

@@ -8,6 +8,7 @@ import Download from 'container/Download/Download';
import { filterDropdown } from 'container/ServiceApplication/Filter/FilterDropdown';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
@@ -31,7 +32,7 @@ function TopOperationsTable({
}: TopOperationsTableProps): JSX.Element {
const searchInput = useRef<InputRef>(null);
const { servicename: encodedServiceName } = useParams<IServiceName>();
const { safeNavigate } = useSafeNavigate();
const servicename = decodeURIComponent(encodedServiceName);
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@@ -87,6 +88,7 @@ function TopOperationsTable({
maxTime,
selectedTraceTags,
apmToTraceQuery: preparedQuery,
safeNavigate,
});
};
@@ -126,7 +128,7 @@ function TopOperationsTable({
key: 'p50',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => a.p50 - b.p50,
render: (value: number): string => (value / 1000000).toFixed(2),
render: (value: number): string => (value / 1_000_000).toFixed(2),
},
{
title: 'P95 (in ms)',
@@ -134,7 +136,7 @@ function TopOperationsTable({
key: 'p95',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => a.p95 - b.p95,
render: (value: number): string => (value / 1000000).toFixed(2),
render: (value: number): string => (value / 1_000_000).toFixed(2),
},
{
title: 'P99 (in ms)',
@@ -142,7 +144,7 @@ function TopOperationsTable({
key: 'p99',
width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
render: (value: number): string => (value / 1_000_000).toFixed(2),
},
{
title: 'Number of Calls',

View File

@@ -21,6 +21,7 @@ export interface NavigateToTraceProps {
maxTime: number;
selectedTraceTags: string;
apmToTraceQuery: Query;
safeNavigate: (path: string) => void;
}
export interface DatabaseCallsRPSProps extends DatabaseCallProps {

View File

@@ -1,6 +1,5 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
@@ -19,10 +18,14 @@ export const navigateToTrace = ({
maxTime,
selectedTraceTags,
apmToTraceQuery,
safeNavigate,
}: NavigateToTraceProps): void => {
const urlParams = new URLSearchParams();
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
urlParams.set(
QueryParams.startTime,
Math.floor(minTime / 1_000_000).toString(),
);
urlParams.set(QueryParams.endTime, Math.floor(maxTime / 1_000_000).toString());
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery));
@@ -32,7 +35,7 @@ export const navigateToTrace = ({
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
history.push(newTraceExplorerPath);
safeNavigate(newTraceExplorerPath);
};
export const getNearestHighestBucketValue = (

View File

@@ -25,6 +25,12 @@ jest.mock(
},
);
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Dashboard landing page actions header tests', () => {
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
const mockLocation = {

View File

@@ -21,8 +21,8 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { isEmpty } from 'lodash-es';
import {
Check,
@@ -89,6 +89,7 @@ export function sanitizeDashboardData(
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { handle } = props;
const {
selectedDashboard,
@@ -311,7 +312,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
}
return (

View File

@@ -150,11 +150,13 @@ function VariableItem({
let allSelected = false;
// The default value for multi-select is ALL and first value for
// single select
if (variableData.multiSelect) {
value = newOptionsData;
allSelected = true;
} else {
[value] = newOptionsData;
if (valueNotInList) {
if (variableData.multiSelect) {
value = newOptionsData;
allSelected = true;
} else {
[value] = newOptionsData;
}
}
if (variableData && variableData?.name && variableData?.id) {
@@ -226,6 +228,9 @@ function VariableItem({
}
setErrorMessage(message);
}
setVariablesToGetUpdated((prev) =>
prev.filter((v) => v !== variableData.name),
);
},
},
);

View File

@@ -3,11 +3,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import {
Dispatch,
SetStateAction,
@@ -33,6 +33,7 @@ function WidgetGraph({
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
@@ -71,9 +72,9 @@ function WidgetGraph({
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
safeNavigate(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
[dispatch, location.pathname, safeNavigate, urlQuery],
);
useEffect(() => {

View File

@@ -87,6 +87,12 @@ jest.mock('hooks/queryBuilder/useGetCompositeQueryParam', () => ({
useGetCompositeQueryParam: (): Query => compositeQueryParam as Query,
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('Column unit selector panel unit test', () => {
it('unit selectors should be rendered for queries and formula', () => {
const mockLocation = {

View File

@@ -20,10 +20,10 @@ import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history';
import { defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
@@ -67,6 +67,7 @@ import {
} from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
setSelectedDashboard,
@@ -328,7 +329,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
updatedQuery.builder.queryData[0].pageSize = 10;
redirectWithQueryBuilderData(updatedQuery);
// If stagedQuery exists, don't re-run the query (e.g. when clicking on Add to Dashboard from logs and traces explorer)
if (!stagedQuery) {
redirectWithQueryBuilderData(updatedQuery);
}
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
@@ -469,7 +474,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard);
setToScrollWidgetId(selectedWidget?.id || '');
history.push({
safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
},
@@ -492,6 +497,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setSelectedDashboard,
setToScrollWidgetId,
setSelectedRowWidgetId,
safeNavigate,
dashboardId,
]);
@@ -500,12 +506,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setDiscardModal(true);
return;
}
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified]);
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified, safeNavigate]);
const discardChanges = useCallback(() => {
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId]);
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, safeNavigate]);
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);

View File

@@ -23,6 +23,12 @@ jest.mock('providers/Dashboard/Dashboard', () => ({
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
describe('QueryTable -', () => {
it('should render correctly with all the data rows', () => {
const { container } = render(<QueryTable {...QueryTableProps} />);

View File

@@ -222,6 +222,8 @@ export const routesToSkip = [
ROUTES.METRICS_EXPLORER,
ROUTES.METRICS_EXPLORER_EXPLORER,
ROUTES.METRICS_EXPLORER_VIEWS,
ROUTES.CHANNELS_NEW,
ROUTES.CHANNELS_EDIT,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -23,17 +23,18 @@ import { QueryHistoryState } from 'container/LiveLogs/types';
import NewExplorerCTA from 'container/NewExplorerCTA';
import dayjs, { Dayjs } from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { isObject } from 'lodash-es';
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { connect, useDispatch, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { useNavigationType } from 'react-router-dom-v5-compat';
import { useCopyToClipboard } from 'react-use';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -43,6 +44,7 @@ import AppActions from 'types/actions';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { normalizeTimeToMs } from 'utils/timeUtils';
import AutoRefresh from '../AutoRefreshV2';
import { DateTimeRangeType } from '../CustomDateTimeModal';
@@ -75,6 +77,9 @@ function DateTimeSelection({
modalSelectedInterval,
}: Props): JSX.Element {
const [formSelector] = Form.useForm();
const { safeNavigate } = useSafeNavigate();
const navigationType = useNavigationType(); // Returns 'POP' for back/forward navigation
const dispatch = useDispatch();
const [hasSelectedTimeError, setHasSelectedTimeError] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
@@ -189,8 +194,8 @@ function DateTimeSelection({
const path = `${ROUTES.LIVE_LOGS}?${QueryParams.compositeQuery}=${JSONCompositeQuery}`;
history.push(path, queryHistoryState);
}, [panelType, queryClient, stagedQuery]);
safeNavigate(path, { state: queryHistoryState });
}, [panelType, queryClient, safeNavigate, stagedQuery]);
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
@@ -349,7 +354,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.relativeTime, value);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
}
// For logs explorer - time range handling is managed in useCopyLogLink.ts:52
@@ -368,6 +373,7 @@ function DateTimeSelection({
location.pathname,
onTimeChange,
refreshButtonHidden,
safeNavigate,
stagedQuery,
updateLocalStorageForRoutes,
updateTimeInterval,
@@ -440,7 +446,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.endTime, endTime?.toDate().getTime().toString());
urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
}
}
}
@@ -467,7 +473,7 @@ function DateTimeSelection({
urlQuery.set(QueryParams.relativeTime, dateTimeStr);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
}
if (!stagedQuery) {
@@ -509,6 +515,77 @@ function DateTimeSelection({
return time;
};
const handleAbsoluteTimeSync = useCallback(
(
startTime: string,
endTime: string,
currentMinTime: number,
currentMaxTime: number,
): void => {
const startTs = normalizeTimeToMs(startTime);
const endTs = normalizeTimeToMs(endTime);
const timeComparison = {
url: {
start: dayjs(startTs).startOf('minute'),
end: dayjs(endTs).startOf('minute'),
},
current: {
start: dayjs(normalizeTimeToMs(currentMinTime)).startOf('minute'),
end: dayjs(normalizeTimeToMs(currentMaxTime)).startOf('minute'),
},
};
const hasTimeChanged =
!timeComparison.current.start.isSame(timeComparison.url.start) ||
!timeComparison.current.end.isSame(timeComparison.url.end);
if (hasTimeChanged) {
dispatch(UpdateTimeInterval('custom', [startTs, endTs]));
}
},
[dispatch],
);
const handleRelativeTimeSync = useCallback(
(relativeTime: string): void => {
updateTimeInterval(relativeTime as Time);
setIsValidteRelativeTime(true);
setRefreshButtonHidden(false);
},
[updateTimeInterval],
);
// Sync time picker state with URL on browser navigation
useEffect(() => {
if (navigationType !== 'POP') return;
if (searchStartTime && searchEndTime) {
handleAbsoluteTimeSync(searchStartTime, searchEndTime, minTime, maxTime);
return;
}
if (
relativeTimeFromUrl &&
isValidTimeFormat(relativeTimeFromUrl) &&
relativeTimeFromUrl !== selectedTime
) {
handleRelativeTimeSync(relativeTimeFromUrl);
}
}, [
navigationType,
searchStartTime,
searchEndTime,
relativeTimeFromUrl,
selectedTime,
minTime,
maxTime,
dispatch,
updateTimeInterval,
handleAbsoluteTimeSync,
handleRelativeTimeSync,
]);
// this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => {
const metricsTimeDuration = getLocalStorageKey(
@@ -524,6 +601,16 @@ function DateTimeSelection({
const currentRoute = location.pathname;
// Give priority to relativeTime from URL if it exists and start /end time are not present in the url, to sync the relative time in URL param with the time picker
if (
!searchStartTime &&
!searchEndTime &&
relativeTimeFromUrl &&
isValidTimeFormat(relativeTimeFromUrl)
) {
handleRelativeTimeSync(relativeTimeFromUrl);
}
// set the default relative time for alert history and overview pages if relative time is not specified
if (
(!urlQuery.has(QueryParams.startTime) ||
@@ -535,7 +622,7 @@ function DateTimeSelection({
updateTimeInterval(defaultRelativeTime);
urlQuery.set(QueryParams.relativeTime, defaultRelativeTime);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
return;
}
@@ -573,7 +660,7 @@ function DateTimeSelection({
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
safeNavigate(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, updateTimeInterval, globalTimeLoading]);

View File

@@ -12,6 +12,7 @@ import {
transformDataWithDate,
} from 'container/TracesExplorer/ListView/utils';
import { Pagination } from 'hooks/queryPagination';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
@@ -39,6 +40,7 @@ function TracesTableComponent({
offset: 0,
limit: 10,
});
const { safeNavigate } = useSafeNavigate();
useEffect(() => {
setRequestData((prev) => ({
@@ -87,6 +89,25 @@ function TracesTableComponent({
[],
);
const handlePaginationChange = useCallback(
(newPagination: Pagination) => {
const urlQuery = new URLSearchParams(window.location.search);
// Update URL with new pagination values
urlQuery.set('offset', newPagination.offset.toString());
urlQuery.set('limit', newPagination.limit.toString());
// Update URL without page reload
safeNavigate({
search: urlQuery.toString(),
});
// Update component state
setPagination(newPagination);
},
[safeNavigate],
);
if (queryResponse.isError) {
return <div>{SOMETHING_WENT_WRONG}</div>;
}
@@ -116,19 +137,19 @@ function TracesTableComponent({
offset={pagination.offset}
countPerPage={pagination.limit}
handleNavigatePrevious={(): void => {
setPagination({
handlePaginationChange({
...pagination,
offset: pagination.offset - pagination.limit,
});
}}
handleNavigateNext={(): void => {
setPagination({
handlePaginationChange({
...pagination,
offset: pagination.offset + pagination.limit,
});
}}
handleCountItemsPerPageChange={(value): void => {
setPagination({
handlePaginationChange({
...pagination,
limit: value,
offset: 0,

View File

@@ -1,7 +1,7 @@
import { Form } from 'antd';
import { FormInstance } from 'antd/lib';
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useUpdateAccountConfig } from 'hooks/integrations/aws/useUpdateAccountConfig';
import { useUpdateAccountConfig } from 'hooks/integration/aws/useUpdateAccountConfig';
import { isEqual } from 'lodash-es';
import {
Dispatch,

View File

@@ -1,4 +1,4 @@
import { getAwsAccounts } from 'api/integrations/aws';
import { getAwsAccounts } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { CloudAccount } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';

View File

@@ -1,4 +1,4 @@
import { getConnectionParams } from 'api/integrations/aws';
import { getConnectionParams } from 'api/integration/aws';
import { AxiosError } from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';

View File

@@ -1,4 +1,4 @@
import { generateConnectionUrl } from 'api/integrations/aws';
import { generateConnectionUrl } from 'api/integration/aws';
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult } from 'react-query';
import {

View File

@@ -1,4 +1,4 @@
import { getAwsServices } from 'api/integrations/aws';
import { getAwsServices } from 'api/integration/aws';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Service } from 'container/CloudIntegrationPage/ServicesSection/types';
import { useQuery, UseQueryResult } from 'react-query';

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