Compare commits
38 Commits
v0.72.0-aw
...
v0.73.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
918c8942c4 | ||
|
|
7e1301b8d2 | ||
|
|
eba2049a4d | ||
|
|
2a939e813d | ||
|
|
c3951afdfd | ||
|
|
fbf0e4efc7 | ||
|
|
1f52139ed3 | ||
|
|
e86c7c970a | ||
|
|
8bfca9b564 | ||
|
|
5a107f33f2 | ||
|
|
a6cfb63036 | ||
|
|
042f31116a | ||
|
|
e7587612e7 | ||
|
|
43a1fa53aa | ||
|
|
966f11fd5c | ||
|
|
52f41e0064 | ||
|
|
82d84c041c | ||
|
|
ef635b6b60 | ||
|
|
169bd3f65b | ||
|
|
50ecf768fa | ||
|
|
2d6131c291 | ||
|
|
7359c0a825 | ||
|
|
846b6a9346 | ||
|
|
bcf7bf38fc | ||
|
|
5511c0230b | ||
|
|
7a03a09ac1 | ||
|
|
0d281e694d | ||
|
|
b772a0fb56 | ||
|
|
962e75c6d4 | ||
|
|
42fad23cb0 | ||
|
|
d22ecb9f7c | ||
|
|
02c2b55d5e | ||
|
|
9a75e27ec3 | ||
|
|
1fb3953614 | ||
|
|
37558facbe | ||
|
|
12f65f4a72 | ||
|
|
398760006b | ||
|
|
37323a64cf |
2
.github/workflows/push.yaml
vendored
2
.github/workflows/push.yaml
vendored
@@ -68,6 +68,8 @@ jobs:
|
||||
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
||||
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
|
||||
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
- query-service
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:0.71.0
|
||||
image: signoz/query-service:0.72.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.71.0
|
||||
image: signoz/frontend:0.72.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.26
|
||||
image: signoz/signoz-otel-collector:0.111.28
|
||||
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.28
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
- query-service
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:0.71.0
|
||||
image: signoz/query-service:0.72.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.71.0
|
||||
image: signoz/frontend:0.72.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.26
|
||||
image: signoz/signoz-otel-collector:0.111.28
|
||||
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.28
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -188,7 +188,7 @@ services:
|
||||
condition: service_healthy
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.72.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.71.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.72.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.26}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.28}
|
||||
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.28}
|
||||
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.28}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
condition: service_healthy
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.72.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.71.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.72.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.26}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.27}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
condition: service_healthy
|
||||
query-service:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.71.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.72.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.71.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.72.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.26}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.28}
|
||||
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.28}
|
||||
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.28}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -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
36
ee/http/middleware/pat.go
Normal 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)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -395,174 +395,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.160.3",
|
||||
"posthog-js": "1.215.5",
|
||||
"rc-tween-one": "3.0.6",
|
||||
"react": "18.2.0",
|
||||
"react-addons-update": "15.6.3",
|
||||
|
||||
@@ -110,6 +110,18 @@ function App(): JSX.Element {
|
||||
source: 'signoz-ui',
|
||||
isPaidUser: !!licenses?.trialConvertedToSubscription,
|
||||
});
|
||||
|
||||
if (
|
||||
window.cioanalytics &&
|
||||
typeof window.cioanalytics.identify === 'function'
|
||||
) {
|
||||
window.cioanalytics.reset();
|
||||
window.cioanalytics.identify(email, {
|
||||
name: user.name,
|
||||
email,
|
||||
role: user.role,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[hostname, isFetchingLicenses, licenses, org],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -407,3 +407,13 @@ export const HAVING_OPERATORS: string[] = [
|
||||
OPERATORS['<='],
|
||||
OPERATORS['<'],
|
||||
];
|
||||
|
||||
export enum PanelDisplay {
|
||||
TIME_SERIES = 'Time Series',
|
||||
VALUE = 'Number',
|
||||
TABLE = 'Table',
|
||||
LIST = 'List',
|
||||
BAR = 'Bar',
|
||||
PIE = 'Pie',
|
||||
HISTOGRAM = 'Histogram',
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ function RenderConnectionFields({
|
||||
isConnectionParamsLoading ||
|
||||
(!!connectionParams?.ingestion_url &&
|
||||
!!connectionParams?.ingestion_key &&
|
||||
!!connectionParams?.signoz_api_url)
|
||||
!!connectionParams?.signoz_api_url &&
|
||||
!!connectionParams?.signoz_api_key)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -48,6 +49,15 @@ function RenderConnectionFields({
|
||||
<Input placeholder="Enter SigNoz API URL" disabled={isFormDisabled} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{!connectionParams?.signoz_api_key && (
|
||||
<Form.Item
|
||||
name="signoz_api_key"
|
||||
label="SigNoz API KEY"
|
||||
rules={[{ required: true, message: 'Please enter SigNoz API Key' }]}
|
||||
>
|
||||
<Input placeholder="Enter SigNoz API Key" disabled={isFormDisabled} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -27,6 +27,12 @@ jest.mock('uplot', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
let mockWindowOpen: jest.Mock;
|
||||
|
||||
window.ResizeObserver =
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
selectedDashboard,
|
||||
isDashboardLocked,
|
||||
handleToggleDashboardSlider,
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const { user } = useAppContext();
|
||||
@@ -34,6 +35,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from 'store';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
MENUITEM_KEYS_VS_LABELS,
|
||||
MenuItemKeys,
|
||||
} from '../WidgetHeader/contants';
|
||||
import { WidgetGraphComponentProps } from './types';
|
||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.DASHBOARD}/624652db-6097-42f5-bbca-e9012901db00`,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockProps: WidgetGraphComponentProps = {
|
||||
widget: {
|
||||
bucketCount: 30,
|
||||
bucketWidth: 0,
|
||||
columnUnits: {},
|
||||
description: '',
|
||||
fillSpans: false,
|
||||
id: '17f905f6-d355-46bd-a78e-cbc87e6f58cc',
|
||||
isStacked: false,
|
||||
mergeAllActiveQueries: false,
|
||||
nullZeroValues: 'zero',
|
||||
opacity: '1',
|
||||
panelTypes: PANEL_TYPES.VALUE,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
aggregateAttribute: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'span_id--string----true',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
key: 'span_id',
|
||||
type: '',
|
||||
},
|
||||
aggregateOperator: 'count_distinct',
|
||||
dataSource: DataSource.TRACES,
|
||||
disabled: false,
|
||||
expression: 'A',
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: '',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
reduceTo: 'last',
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'count_distinct',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
clickhouse_sql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
id: '47449208-2c76-4465-9c62-a37fb4f5f11f',
|
||||
promql: [
|
||||
{
|
||||
disabled: false,
|
||||
legend: '',
|
||||
name: 'A',
|
||||
query: '',
|
||||
},
|
||||
],
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
selectedLogFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
name: 'body',
|
||||
type: '',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
name: 'timestamp',
|
||||
type: '',
|
||||
},
|
||||
],
|
||||
selectedTracesFields: [],
|
||||
softMax: 0,
|
||||
softMin: 0,
|
||||
stackedBarChart: false,
|
||||
thresholds: [],
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title: 'Test Dashboard',
|
||||
yAxisUnit: 'none',
|
||||
},
|
||||
queryResponse: {
|
||||
status: 'loading',
|
||||
isLoading: true,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
isIdle: false,
|
||||
dataUpdatedAt: 0,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: false,
|
||||
isFetchedAfterMount: false,
|
||||
isFetching: true,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isStale: true,
|
||||
data: undefined,
|
||||
refetch: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
errorMessage: '',
|
||||
version: 'v4',
|
||||
headerMenuList: [
|
||||
MenuItemKeys.View,
|
||||
MenuItemKeys.Clone,
|
||||
MenuItemKeys.Delete,
|
||||
MenuItemKeys.Edit,
|
||||
MenuItemKeys.CreateAlerts,
|
||||
],
|
||||
isWarning: false,
|
||||
isFetchingResponse: false,
|
||||
setRequestData: jest.fn(),
|
||||
onClickHandler: jest.fn(),
|
||||
onDragSelect: jest.fn(),
|
||||
openTracesButton: false,
|
||||
onOpenTraceBtnClick: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock useDashabord hook
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
selectedDashboard: {
|
||||
data: {
|
||||
variables: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('WidgetGraphComponent', () => {
|
||||
it('should show correct menu items when hovering over more options while loading', async () => {
|
||||
const { getByTestId, findByRole, getByText, container } = render(
|
||||
<MockQueryClientProvider>
|
||||
<Provider store={store}>
|
||||
<AppProvider>
|
||||
<WidgetGraphComponent
|
||||
widget={mockProps.widget}
|
||||
queryResponse={mockProps.queryResponse}
|
||||
errorMessage={mockProps.errorMessage}
|
||||
version={mockProps.version}
|
||||
headerMenuList={mockProps.headerMenuList}
|
||||
isWarning={mockProps.isWarning}
|
||||
isFetchingResponse={mockProps.isFetchingResponse}
|
||||
setRequestData={mockProps.setRequestData}
|
||||
onClickHandler={mockProps.onClickHandler}
|
||||
onDragSelect={mockProps.onDragSelect}
|
||||
openTracesButton={mockProps.openTracesButton}
|
||||
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
|
||||
/>
|
||||
</AppProvider>
|
||||
</Provider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(getByText('Test Dashboard')).toBeInTheDocument();
|
||||
|
||||
// check if skeleton is rendered
|
||||
const skeleton = container.querySelector('.ant-skeleton');
|
||||
expect(skeleton).toBeInTheDocument();
|
||||
|
||||
const moreOptionsButton = getByTestId('widget-header-options');
|
||||
fireEvent.mouseEnter(moreOptionsButton);
|
||||
|
||||
const menu = await findByRole('menu');
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
// Check if all menu items are present
|
||||
const expectedMenuItems = [
|
||||
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.View],
|
||||
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Clone],
|
||||
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Delete],
|
||||
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Edit],
|
||||
MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
|
||||
];
|
||||
|
||||
// check that menu is visible
|
||||
expectedMenuItems.forEach((item) => {
|
||||
expect(screen.getByText(item)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,12 +6,13 @@ import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
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 {
|
||||
@@ -50,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();
|
||||
@@ -133,18 +135,14 @@ function WidgetGraphComponent({
|
||||
(l) => l.i === widget.id,
|
||||
);
|
||||
|
||||
// added the cloned panel on the top as it is given most priority when arranging
|
||||
// in the layout. React_grid_layout assigns priority from top, hence no random position for cloned panel
|
||||
const layout = [
|
||||
{
|
||||
i: uuid,
|
||||
w: originalPanelLayout?.w || 6,
|
||||
x: 0,
|
||||
h: originalPanelLayout?.h || 6,
|
||||
y: 0,
|
||||
},
|
||||
...(selectedDashboard.data.layout || []),
|
||||
];
|
||||
const newLayoutItem = placeWidgetAtBottom(
|
||||
uuid,
|
||||
selectedDashboard?.data.layout || [],
|
||||
originalPanelLayout?.w || 6,
|
||||
originalPanelLayout?.h || 6,
|
||||
);
|
||||
|
||||
const layout = [...(selectedDashboard.data.layout || []), newLayoutItem];
|
||||
|
||||
updateDashboardMutation.mutateAsync(
|
||||
{
|
||||
@@ -176,7 +174,7 @@ function WidgetGraphComponent({
|
||||
graphType: widget?.panelTypes,
|
||||
widgetId: uuid,
|
||||
};
|
||||
history.push(`${pathname}/new?${createQueryParams(queryParams)}`);
|
||||
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -197,7 +195,7 @@ function WidgetGraphComponent({
|
||||
const separator = existingSearch.toString() ? '&' : '';
|
||||
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
|
||||
|
||||
history.push({
|
||||
safeNavigate({
|
||||
pathname,
|
||||
search: newSearch,
|
||||
});
|
||||
@@ -224,7 +222,7 @@ function WidgetGraphComponent({
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
history.push({
|
||||
safeNavigate({
|
||||
pathname,
|
||||
search: createQueryParams(updatedQueryParams),
|
||||
});
|
||||
@@ -232,21 +230,6 @@ function WidgetGraphComponent({
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
const loadingState =
|
||||
(queryResponse.isLoading || queryResponse.status === 'idle') &&
|
||||
widget.panelTypes !== PANEL_TYPES.LIST;
|
||||
|
||||
if (loadingState) {
|
||||
return (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface GridCardGraphProps {
|
||||
customErrorMessage?: string;
|
||||
start?: number;
|
||||
end?: number;
|
||||
analyticsEvent?: string;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
|
||||
.menu-content {
|
||||
.section-1 {
|
||||
.rename-btn {
|
||||
.rename-btn,
|
||||
.new-panel-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@@ -150,6 +151,10 @@
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.rename-btn {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-2 {
|
||||
|
||||
@@ -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 {
|
||||
@@ -45,6 +45,7 @@ import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
|
||||
import GridCard from './GridCard';
|
||||
import { Card, CardContainer, ReactGridLayout } from './styles';
|
||||
import { removeUndefinedValuesFromLayout } from './utils';
|
||||
import { MenuItemKeys } from './WidgetHeader/contants';
|
||||
import { WidgetRowHeader } from './WidgetRow';
|
||||
|
||||
interface GraphLayoutProps {
|
||||
@@ -54,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,
|
||||
@@ -64,6 +66,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
isDashboardLocked,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
setSelectedRowWidgetId,
|
||||
isDashboardFetching,
|
||||
} = useDashboard();
|
||||
const { data } = selectedDashboard || {};
|
||||
const { pathname } = useLocation();
|
||||
@@ -173,6 +177,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
setSelectedRowWidgetId(null);
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
@@ -190,7 +195,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
const widgetActions = !isDashboardLocked
|
||||
? [...ViewMenuAction, ...EditMenuAction]
|
||||
: [...ViewMenuAction];
|
||||
: [...ViewMenuAction, MenuItemKeys.CreateAlerts];
|
||||
|
||||
const handleLayoutChange = (layout: Layout[]): void => {
|
||||
const filterLayout = removeUndefinedValuesFromLayout(layout);
|
||||
@@ -211,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(() => {
|
||||
@@ -228,7 +233,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
!isEqual(layouts, dashboardLayout) &&
|
||||
!isDashboardLocked &&
|
||||
saveLayoutPermission &&
|
||||
!updateDashboardMutation.isLoading
|
||||
!updateDashboardMutation.isLoading &&
|
||||
!isDashboardFetching
|
||||
) {
|
||||
onSaveHandler();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Button, Popover } from 'antd';
|
||||
import { EllipsisIcon, PenLine, X } from 'lucide-react';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useState } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
interface WidgetRowHeaderProps {
|
||||
rowWidgetProperties: {
|
||||
@@ -27,6 +32,23 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
||||
id,
|
||||
} = props;
|
||||
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
handleToggleDashboardSlider,
|
||||
selectedDashboard,
|
||||
isDashboardLocked,
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const permissions: ComponentTypes[] = ['add_panel'];
|
||||
const { user } = useAppContext();
|
||||
|
||||
const userRole: ROLES | null =
|
||||
selectedDashboard?.created_by === user?.email
|
||||
? (USER_ROLES.AUTHOR as ROLES)
|
||||
: user.role;
|
||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isRowSettingsOpen}
|
||||
@@ -52,6 +74,20 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
||||
Rename
|
||||
</Button>
|
||||
</section>
|
||||
<section className="section-1">
|
||||
<Button
|
||||
className="new-panel-btn"
|
||||
type="text"
|
||||
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
setSelectedRowWidgetId(id);
|
||||
handleToggleDashboardSlider(true);
|
||||
}}
|
||||
>
|
||||
New Panel
|
||||
</Button>
|
||||
</section>
|
||||
{!rowWidgetProperties.collapsed && (
|
||||
<section className="section-2">
|
||||
<Button
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -290,6 +291,7 @@ function Application(): JSX.Element {
|
||||
},
|
||||
],
|
||||
});
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -317,6 +319,7 @@ function Application(): JSX.Element {
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
>
|
||||
View Traces
|
||||
@@ -346,6 +349,7 @@ function Application(): JSX.Element {
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
>
|
||||
View Traces
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -36,6 +35,7 @@ interface OnViewTracePopupClickProps {
|
||||
apmToTraceQuery: Query;
|
||||
isViewLogsClicked?: boolean;
|
||||
stepInterval?: number;
|
||||
safeNavigate: (url: string) => void;
|
||||
}
|
||||
|
||||
export function generateExplorerPath(
|
||||
@@ -63,6 +63,7 @@ export function onViewTracePopupClick({
|
||||
apmToTraceQuery,
|
||||
isViewLogsClicked,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewTracePopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = timestamp;
|
||||
@@ -88,7 +89,7 @@ export function onViewTracePopupClick({
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.push(newPath);
|
||||
safeNavigate(newPath);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ export function onGraphClickHandler(
|
||||
buttonElement.style.display = 'block';
|
||||
buttonElement.style.left = `${mouseX}px`;
|
||||
buttonElement.style.top = `${mouseY}px`;
|
||||
setSelectedTimeStamp(xValue);
|
||||
setSelectedTimeStamp(Math.floor(xValue * 1_000));
|
||||
}
|
||||
} else if (buttonElement && buttonElement.style.display === 'block') {
|
||||
buttonElement.style.display = 'none';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface NavigateToTraceProps {
|
||||
maxTime: number;
|
||||
selectedTraceTags: string;
|
||||
apmToTraceQuery: Query;
|
||||
safeNavigate: (path: string) => void;
|
||||
}
|
||||
|
||||
export interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||
import {
|
||||
BarChart3,
|
||||
LineChart,
|
||||
@@ -13,37 +13,37 @@ const Items: ItemsProps[] = [
|
||||
{
|
||||
name: PANEL_TYPES.TIME_SERIES,
|
||||
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Time Series',
|
||||
display: PanelDisplay.TIME_SERIES,
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.VALUE,
|
||||
icon: <SigmaSquare size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Value',
|
||||
display: PanelDisplay.VALUE,
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.TABLE,
|
||||
icon: <Table size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Table',
|
||||
display: PanelDisplay.TABLE,
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.LIST,
|
||||
icon: <List size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'List',
|
||||
display: PanelDisplay.LIST,
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.BAR,
|
||||
icon: <BarChart3 size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Bar',
|
||||
display: PanelDisplay.BAR,
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.PIE,
|
||||
icon: <PieChart size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Pie',
|
||||
display: PanelDisplay.PIE,
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.HISTOGRAM,
|
||||
icon: <BarChart3 size={16} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Histogram',
|
||||
display: PanelDisplay.HISTOGRAM,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
@@ -100,6 +101,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
listSortOrder,
|
||||
setSelectedDashboard,
|
||||
handleToggleDashboardSlider,
|
||||
setSelectedRowWidgetId,
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
@@ -157,6 +159,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
@@ -309,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 (
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import './RightContainer.styles.scss';
|
||||
|
||||
import { Input, InputNumber, Select, Space, Switch, Typography } from 'antd';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||
import GraphTypes, {
|
||||
ItemsProps,
|
||||
} from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
@@ -212,7 +212,8 @@ function RightContainer({
|
||||
defaultValue={yAxisUnit}
|
||||
onSelect={setYAxisUnit}
|
||||
fieldLabel={
|
||||
selectedGraphType === 'Value' || selectedGraphType === 'Pie'
|
||||
selectedGraphType === PanelDisplay.VALUE ||
|
||||
selectedGraphType === PanelDisplay.PIE
|
||||
? 'Unit'
|
||||
: 'Y Axis Unit'
|
||||
}
|
||||
|
||||
218
frontend/src/container/NewWidget/__test__/NewWidget.test.tsx
Normal file
218
frontend/src/container/NewWidget/__test__/NewWidget.test.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
// This test suite covers several important scenarios:
|
||||
// - Empty layout - widget should be placed at origin (0,0)
|
||||
// - Empty layout with custom dimensions
|
||||
// - Placing widget next to an existing widget when there's space in the last row
|
||||
// - Placing widget at bottom when the last row is full
|
||||
// - Handling multiple rows correctly
|
||||
// - Handling widgets with different heights
|
||||
|
||||
import { placeWidgetAtBottom, placeWidgetBetweenRows } from '../utils';
|
||||
|
||||
describe('placeWidgetAtBottom', () => {
|
||||
it('should place widget at (0,0) when layout is empty', () => {
|
||||
const result = placeWidgetAtBottom('widget1', []);
|
||||
expect(result).toEqual({
|
||||
i: 'widget1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('should place widget at (0,0) with custom dimensions when layout is empty', () => {
|
||||
const result = placeWidgetAtBottom('widget1', [], 4, 8);
|
||||
expect(result).toEqual({
|
||||
i: 'widget1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('should place widget next to existing widget in last row if space available', () => {
|
||||
const existingLayout = [{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 }];
|
||||
const result = placeWidgetAtBottom('widget2', existingLayout);
|
||||
expect(result).toEqual({
|
||||
i: 'widget2',
|
||||
x: 6,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('should place widget at bottom when last row is full', () => {
|
||||
const existingLayout = [
|
||||
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
|
||||
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
|
||||
];
|
||||
const result = placeWidgetAtBottom('widget3', existingLayout);
|
||||
expect(result).toEqual({
|
||||
i: 'widget3',
|
||||
x: 0,
|
||||
y: 6,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple rows correctly', () => {
|
||||
const existingLayout = [
|
||||
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
|
||||
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
|
||||
{ i: 'widget3', x: 0, y: 6, w: 6, h: 6 },
|
||||
];
|
||||
const result = placeWidgetAtBottom('widget4', existingLayout);
|
||||
expect(result).toEqual({
|
||||
i: 'widget4',
|
||||
x: 6,
|
||||
y: 6,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle widgets with different heights', () => {
|
||||
const existingLayout = [
|
||||
{ i: 'widget1', x: 0, y: 0, w: 6, h: 8 },
|
||||
{ i: 'widget2', x: 6, y: 0, w: 6, h: 4 },
|
||||
];
|
||||
const result = placeWidgetAtBottom('widget3', existingLayout);
|
||||
// y = 2 here as later the react-grid-layout will add 2px to the y value while adjusting the layout
|
||||
expect(result).toEqual({
|
||||
i: 'widget3',
|
||||
x: 6,
|
||||
y: 2,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeWidgetBetweenRows', () => {
|
||||
it('should return single widget layout when layout is empty', () => {
|
||||
const result = placeWidgetBetweenRows('widget1', [], 'currentRow');
|
||||
expect(result).toEqual([
|
||||
{
|
||||
i: 'widget1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should place widget at the end of the layout when no nextRowId is provided', () => {
|
||||
const existingLayout = [
|
||||
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
|
||||
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
|
||||
];
|
||||
|
||||
const result = placeWidgetBetweenRows('widget3', existingLayout, 'widget2');
|
||||
|
||||
expect(result).toEqual([
|
||||
{ i: 'widget1', x: 0, y: 0, w: 6, h: 6 },
|
||||
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
|
||||
{ i: 'widget3', x: 0, y: 6, w: 6, h: 6 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should place widget between current and next row', () => {
|
||||
const existingLayout = [
|
||||
{
|
||||
h: 1,
|
||||
i: "'widget1'",
|
||||
maxH: 1,
|
||||
minH: 1,
|
||||
minW: 12,
|
||||
moved: false,
|
||||
static: false,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{ i: 'widget2', x: 6, y: 0, w: 6, h: 6 },
|
||||
{
|
||||
h: 1,
|
||||
i: 'widget3',
|
||||
maxH: 1,
|
||||
minH: 1,
|
||||
minW: 12,
|
||||
moved: false,
|
||||
static: false,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 7,
|
||||
},
|
||||
];
|
||||
|
||||
const result = placeWidgetBetweenRows(
|
||||
'widget4',
|
||||
existingLayout,
|
||||
'widget1',
|
||||
'widget3',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
h: 1,
|
||||
i: "'widget1'",
|
||||
maxH: 1,
|
||||
minH: 1,
|
||||
minW: 12,
|
||||
moved: false,
|
||||
static: false,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
h: 6,
|
||||
i: 'widget2',
|
||||
w: 6,
|
||||
x: 6,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
h: 6,
|
||||
i: 'widget4',
|
||||
w: 6,
|
||||
x: 0,
|
||||
y: 6,
|
||||
},
|
||||
{
|
||||
h: 1,
|
||||
i: 'widget3',
|
||||
maxH: 1,
|
||||
minH: 1,
|
||||
minW: 12,
|
||||
moved: false,
|
||||
static: false,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 7,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should respect custom widget dimensions', () => {
|
||||
const existingLayout = [{ i: 'widget1', x: 0, y: 0, w: 12, h: 4 }];
|
||||
|
||||
const result = placeWidgetBetweenRows(
|
||||
'widget2',
|
||||
existingLayout,
|
||||
'widget1',
|
||||
null,
|
||||
8,
|
||||
3,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ i: 'widget1', x: 0, y: 0, w: 12, h: 4 },
|
||||
{ i: 'widget2', x: 0, y: 4, w: 8, h: 3 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,11 @@ import logEvent from 'api/common/logEvent';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
PANEL_GROUP_TYPES,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
|
||||
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
||||
@@ -16,11 +20,11 @@ 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, isUndefined } from 'lodash-es';
|
||||
import { defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -58,13 +62,18 @@ import {
|
||||
getDefaultWidgetData,
|
||||
getIsQueryModified,
|
||||
handleQueryChange,
|
||||
placeWidgetAtBottom,
|
||||
placeWidgetBetweenRows,
|
||||
} from './utils';
|
||||
|
||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
selectedRowWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
@@ -320,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,
|
||||
@@ -363,20 +376,36 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
const widgetId = query.get('widgetId');
|
||||
const widgetId = query.get('widgetId') || '';
|
||||
let updatedLayout = selectedDashboard.data.layout || [];
|
||||
if (isNewDashboard) {
|
||||
updatedLayout = [
|
||||
{
|
||||
i: widgetId || '',
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 6,
|
||||
y: 0,
|
||||
},
|
||||
...updatedLayout,
|
||||
];
|
||||
|
||||
if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
|
||||
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
|
||||
updatedLayout = [...updatedLayout, newLayoutItem];
|
||||
}
|
||||
|
||||
if (isNewDashboard && selectedRowWidgetId) {
|
||||
// Find the next row by looking through remaining layout items
|
||||
const currentIndex = updatedLayout.findIndex(
|
||||
(e) => e.i === selectedRowWidgetId,
|
||||
);
|
||||
const nextRowIndex = updatedLayout.findIndex(
|
||||
(item, index) =>
|
||||
index > currentIndex &&
|
||||
widgets?.find((w) => w.id === item.i)?.panelTypes ===
|
||||
PANEL_GROUP_TYPES.ROW,
|
||||
);
|
||||
const nextRowId = nextRowIndex !== -1 ? updatedLayout[nextRowIndex].i : null;
|
||||
|
||||
const newLayoutItem = placeWidgetBetweenRows(
|
||||
widgetId,
|
||||
updatedLayout,
|
||||
selectedRowWidgetId,
|
||||
nextRowId,
|
||||
);
|
||||
updatedLayout = newLayoutItem;
|
||||
}
|
||||
|
||||
const dashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
uuid: selectedDashboard.uuid,
|
||||
@@ -442,9 +471,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
updateDashboardMutation.mutateAsync(dashboard, {
|
||||
onSuccess: () => {
|
||||
setSelectedRowWidgetId(null);
|
||||
setSelectedDashboard(dashboard);
|
||||
setToScrollWidgetId(selectedWidget?.id || '');
|
||||
history.push({
|
||||
safeNavigate({
|
||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
||||
});
|
||||
},
|
||||
@@ -454,16 +484,20 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedDashboard,
|
||||
query,
|
||||
isNewDashboard,
|
||||
preWidgets,
|
||||
selectedRowWidgetId,
|
||||
afterWidgets,
|
||||
selectedWidget,
|
||||
selectedTime.enum,
|
||||
graphType,
|
||||
currentQuery,
|
||||
afterWidgets,
|
||||
preWidgets,
|
||||
updateDashboardMutation,
|
||||
handleError,
|
||||
widgets,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
setSelectedRowWidgetId,
|
||||
safeNavigate,
|
||||
dashboardId,
|
||||
]);
|
||||
|
||||
@@ -472,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);
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
PANEL_TYPES_INITIAL_QUERY,
|
||||
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
|
||||
import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es';
|
||||
import { cloneDeep, defaultTo, isEmpty, isEqual, set, unset } from 'lodash-es';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -575,3 +576,98 @@ export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
|
||||
options: getCategorySelectOptionByName(filteredCategory),
|
||||
}));
|
||||
};
|
||||
|
||||
export const placeWidgetAtBottom = (
|
||||
widgetId: string,
|
||||
layout: Layout[],
|
||||
widgetWidth?: number,
|
||||
widgetHeight?: number,
|
||||
): Layout => {
|
||||
if (layout.length === 0) {
|
||||
return { i: widgetId, x: 0, y: 0, w: widgetWidth || 6, h: widgetHeight || 6 };
|
||||
}
|
||||
|
||||
// Find the maximum Y coordinate and height
|
||||
const { maxY } = layout.reduce(
|
||||
(acc, curr) => ({
|
||||
maxY: Math.max(acc.maxY, curr.y + curr.h),
|
||||
}),
|
||||
{ maxY: 0 },
|
||||
);
|
||||
|
||||
// Check for available space in the last row
|
||||
const lastRowWidgets = layout.filter((item) => item.y + item.h === maxY);
|
||||
const occupiedXInLastRow = lastRowWidgets.reduce(
|
||||
(acc, widget) => acc + widget.w,
|
||||
0,
|
||||
);
|
||||
|
||||
// If there's space in the last row (total width < 12)
|
||||
if (occupiedXInLastRow < 12) {
|
||||
// Find the rightmost X coordinate in the last row
|
||||
const maxXInLastRow = lastRowWidgets.reduce(
|
||||
(acc, widget) => Math.max(acc, widget.x + widget.w),
|
||||
0,
|
||||
);
|
||||
|
||||
// If there's enough space for a 6-width widget
|
||||
if (maxXInLastRow + defaultTo(widgetWidth, 6) <= 12) {
|
||||
return {
|
||||
i: widgetId,
|
||||
x: maxXInLastRow,
|
||||
y: maxY - (widgetHeight || 6), // Align with the last row
|
||||
w: widgetWidth || 6,
|
||||
h: widgetHeight || 6,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If no space in last row, place at the bottom
|
||||
return {
|
||||
i: widgetId,
|
||||
x: 0,
|
||||
y: maxY,
|
||||
w: widgetWidth || 6,
|
||||
h: widgetHeight || 6,
|
||||
};
|
||||
};
|
||||
|
||||
export const placeWidgetBetweenRows = (
|
||||
widgetId: string,
|
||||
layout: Layout[],
|
||||
_currentRowId: string,
|
||||
nextRowId?: string | null,
|
||||
widgetWidth?: number,
|
||||
widgetHeight?: number,
|
||||
): Layout[] => {
|
||||
if (layout.length === 0) {
|
||||
return [
|
||||
{
|
||||
i: widgetId,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: widgetWidth || 6,
|
||||
h: widgetHeight || 6,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const nextRowIndex = nextRowId
|
||||
? layout.findIndex((item) => item.i === nextRowId)
|
||||
: -1;
|
||||
|
||||
// slice the layout from current row to next row
|
||||
const sectionWidgets =
|
||||
nextRowIndex === -1 ? layout : layout.slice(0, nextRowIndex);
|
||||
|
||||
const newWidgetLayout = placeWidgetAtBottom(
|
||||
widgetId,
|
||||
sectionWidgets,
|
||||
widgetWidth,
|
||||
widgetHeight,
|
||||
);
|
||||
const remainingWidgets = nextRowIndex === -1 ? [] : layout.slice(nextRowIndex);
|
||||
|
||||
// add new layout in between the sectionWidgets and the rest of the layout
|
||||
return [...sectionWidgets, newWidgetLayout, ...remainingWidgets];
|
||||
};
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -22,20 +23,16 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
|
||||
...convertKeysToColumnFields(selectedColumns || []),
|
||||
];
|
||||
|
||||
const newLayoutItem = placeWidgetAtBottom(
|
||||
widgetId,
|
||||
dashboard?.data?.layout || [],
|
||||
);
|
||||
|
||||
return {
|
||||
...dashboard,
|
||||
data: {
|
||||
...dashboard.data,
|
||||
layout: [
|
||||
{
|
||||
i: widgetId,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 6,
|
||||
y: 0,
|
||||
},
|
||||
...(dashboard?.data?.layout || []),
|
||||
],
|
||||
layout: [...(dashboard?.data?.layout || []), newLayoutItem],
|
||||
widgets: [
|
||||
...(dashboard?.data?.widgets || []),
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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 {
|
||||
@@ -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
Reference in New Issue
Block a user