mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-21 15:50:27 +01:00
Compare commits
57 Commits
v0.86.2
...
chore/tf/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ddf002d46 | ||
|
|
31cd192f24 | ||
|
|
3aacb99adc | ||
|
|
40ca9adbfc | ||
|
|
bfa7a06e90 | ||
|
|
189046865a | ||
|
|
d5ee6ca2c3 | ||
|
|
62fb05ac5a | ||
|
|
5a3ed26f01 | ||
|
|
8e490e4089 | ||
|
|
d3adc319ad | ||
|
|
6b58e859b5 | ||
|
|
3d3a1eaaf2 | ||
|
|
361640fd22 | ||
|
|
89de68f987 | ||
|
|
71b098776b | ||
|
|
6b3e2759a1 | ||
|
|
ea46639f59 | ||
|
|
69ed1b7d02 | ||
|
|
5b07705157 | ||
|
|
2fc8bb4585 | ||
|
|
11d75940e8 | ||
|
|
3caaa51e08 | ||
|
|
cad3bf6883 | ||
|
|
5b7ce41d0d | ||
|
|
ee5120d4ed | ||
|
|
c249620e8f | ||
|
|
1c1811b216 | ||
|
|
86d69f74f3 | ||
|
|
37b26a7116 | ||
|
|
26ad89ed70 | ||
|
|
94c7512a6a | ||
|
|
333ff86a6b | ||
|
|
a22d061ec1 | ||
|
|
22fdeb1381 | ||
|
|
77b330cfe9 | ||
|
|
78a5d7e39e | ||
|
|
ea1f4e8253 | ||
|
|
5e0d6110b5 | ||
|
|
14ce7f80e2 | ||
|
|
f8341e8958 | ||
|
|
7a7428d73e | ||
|
|
77cd490e48 | ||
|
|
9a96817a88 | ||
|
|
da9a2520a4 | ||
|
|
c03bf9905c | ||
|
|
6676832c71 | ||
|
|
03e50d3bc3 | ||
|
|
95bc3987bb | ||
|
|
eb797edc53 | ||
|
|
c2d36480a2 | ||
|
|
ab0d9918b2 | ||
|
|
c364a3e695 | ||
|
|
43337b6697 | ||
|
|
e258d70df5 | ||
|
|
19ee5860cb | ||
|
|
235ea39d73 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -11,5 +11,5 @@
|
||||
/pkg/errors/ @grandwizard28
|
||||
/pkg/factory/ @grandwizard28
|
||||
/pkg/types/ @grandwizard28
|
||||
/pkg/sqlmigration/ @vikrantgupta25
|
||||
.golangci.yml @grandwizard28
|
||||
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25
|
||||
@@ -8,7 +8,7 @@
|
||||
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/signoz.svg?label=Docker%20Downloads"> </a>
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
|
||||
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
|
||||
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.86.2
|
||||
image: signoz/signoz:v0.85.3
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.86.2
|
||||
image: signoz/signoz:v0.85.3
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -177,7 +177,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.86.2}
|
||||
image: signoz/signoz:${VERSION:-v0.85.3}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.86.2}
|
||||
image: signoz/signoz:${VERSION:-v0.85.3}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -3,6 +3,7 @@ package httplicensing
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -86,6 +88,13 @@ func (provider *provider) Validate(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(organizations) == 0 {
|
||||
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -106,6 +115,11 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.InitFeatures(ctx, license.Features)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -125,24 +139,28 @@ func (provider *provider) GetActive(ctx context.Context, organizationID valuer.U
|
||||
|
||||
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
|
||||
activeLicense, err := provider.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil && errors.Ast(err, errors.TypeNotFound) {
|
||||
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "org_id", organizationID.StringValue())
|
||||
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
|
||||
if err != nil {
|
||||
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
|
||||
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
|
||||
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
|
||||
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
|
||||
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals, defaulting to basic plan", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
|
||||
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -200,14 +218,80 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
|
||||
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
|
||||
license, err := provider.GetActive(ctx, organizationID)
|
||||
// feature surrogate
|
||||
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
|
||||
feature, err := provider.store.GetFeature(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if feature.Active {
|
||||
return nil
|
||||
}
|
||||
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
|
||||
}
|
||||
|
||||
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
|
||||
featureStatus, err := provider.store.GetFeature(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &featuretypes.GettableFeature{
|
||||
Name: featureStatus.Name,
|
||||
Active: featureStatus.Active,
|
||||
Usage: int64(featureStatus.Usage),
|
||||
UsageLimit: int64(featureStatus.UsageLimit),
|
||||
Route: featureStatus.Route,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
|
||||
storableFeatures, err := provider.store.GetAllFeatures(ctx)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return licensetypes.BasicPlan, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return license.Features, nil
|
||||
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
|
||||
for idx, gettableFeature := range storableFeatures {
|
||||
gettableFeatures[idx] = &featuretypes.GettableFeature{
|
||||
Name: gettableFeature.Name,
|
||||
Active: gettableFeature.Active,
|
||||
Usage: int64(gettableFeature.Usage),
|
||||
UsageLimit: int64(gettableFeature.UsageLimit),
|
||||
Route: gettableFeature.Route,
|
||||
}
|
||||
}
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
gettableFeatures = append(gettableFeatures, &featuretypes.GettableFeature{
|
||||
Name: featuretypes.DotMetricsEnabled,
|
||||
Active: true,
|
||||
})
|
||||
}
|
||||
|
||||
return gettableFeatures, nil
|
||||
}
|
||||
|
||||
func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
|
||||
featureStatus := make([]*featuretypes.StorableFeature, len(features))
|
||||
for i, f := range features {
|
||||
featureStatus[i] = &featuretypes.StorableFeature{
|
||||
Name: f.Name,
|
||||
Active: f.Active,
|
||||
Usage: int(f.Usage),
|
||||
UsageLimit: int(f.UsageLimit),
|
||||
Route: f.Route,
|
||||
}
|
||||
}
|
||||
|
||||
return provider.store.InitFeatures(ctx, featureStatus)
|
||||
}
|
||||
|
||||
func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
|
||||
return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{
|
||||
Name: feature.Name,
|
||||
Active: feature.Active,
|
||||
Usage: int(feature.Usage),
|
||||
UsageLimit: int(feature.UsageLimit),
|
||||
Route: feature.Route,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -79,3 +80,81 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewInsert().
|
||||
Model(storableFeature).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
|
||||
storableFeature := new(featuretypes.StorableFeature)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(storableFeature).
|
||||
Where("name = ?", key).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
|
||||
}
|
||||
|
||||
return storableFeature, nil
|
||||
}
|
||||
|
||||
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
|
||||
storableFeatures := make([]*featuretypes.StorableFeature, 0)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&storableFeatures).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
|
||||
}
|
||||
|
||||
return storableFeatures, nil
|
||||
}
|
||||
|
||||
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewInsert().
|
||||
Model(&storableFeatures).
|
||||
On("CONFLICT (name) DO UPDATE").
|
||||
Set("active = EXCLUDED.active").
|
||||
Set("usage = EXCLUDED.usage").
|
||||
Set("usage_limit = EXCLUDED.usage_limit").
|
||||
Set("route = EXCLUDED.route").
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewUpdate().
|
||||
Model(storableFeature).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
@@ -85,12 +86,23 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
|
||||
return ah.opts.Gateway
|
||||
}
|
||||
|
||||
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
|
||||
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// RegisterRoutes registers routes for this handler on the given router
|
||||
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
// note: add ee override methods first
|
||||
|
||||
// routes available only in ee version
|
||||
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
|
||||
|
||||
// invite
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
|
||||
|
||||
// paid plans specific routes
|
||||
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
||||
@@ -102,6 +114,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
|
||||
|
||||
// v3
|
||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
|
||||
|
||||
62
ee/query-service/app/api/dashboard.go
Normal file
62
ee/query-service/app/api/dashboard.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ah.lockUnlockDashboard(w, r, true)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ah.lockUnlockDashboard(w, r, false)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
|
||||
// Locking can only be done by the owner of the dashboard
|
||||
// or an admin
|
||||
|
||||
// - Fetch the dashboard
|
||||
// - Check if the user is the owner or an admin
|
||||
// - If yes, lock/unlock the dashboard
|
||||
// - If no, return 403
|
||||
|
||||
// Get the dashboard UUID from the request
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
if strings.HasPrefix(uuid, "integration") {
|
||||
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified"))
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), claims.OrgID, uuid)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
|
||||
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
// Lock/Unlock the dashboard
|
||||
err = ah.Signoz.Modules.Dashboard.LockUnlock(r.Context(), claims.OrgID, uuid, lock)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, "Dashboard updated successfully")
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
pkgError "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
|
||||
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
|
||||
if err != nil {
|
||||
ah.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
@@ -61,15 +61,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if ah.opts.PreferSpanMetrics {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.UseSpanMetrics {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
if feature.Name == featuretypes.UseSpanMetrics {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
@@ -80,7 +72,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
|
||||
// and returns the FeatureSet.
|
||||
func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
|
||||
func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) {
|
||||
// Check if the URL is empty
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("url is empty")
|
||||
@@ -139,28 +131,28 @@ func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error)
|
||||
}
|
||||
|
||||
type ZeusFeaturesResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data []*licensetypes.Feature `json:"data"`
|
||||
Status string `json:"status"`
|
||||
Data []*featuretypes.GettableFeature `json:"data"`
|
||||
}
|
||||
|
||||
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
|
||||
func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
|
||||
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
|
||||
// Create a map to store the merged features
|
||||
featureMap := make(map[string]*licensetypes.Feature)
|
||||
featureMap := make(map[string]*featuretypes.GettableFeature)
|
||||
|
||||
// Add all features from the otherFeatures set to the map
|
||||
for _, feature := range internalFeatures {
|
||||
featureMap[feature.Name.StringValue()] = feature
|
||||
featureMap[feature.Name] = feature
|
||||
}
|
||||
|
||||
// Add all features from the zeusFeatures set to the map
|
||||
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
|
||||
for _, feature := range zeusFeatures {
|
||||
featureMap[feature.Name.StringValue()] = feature
|
||||
featureMap[feature.Name] = feature
|
||||
}
|
||||
|
||||
// Convert the map back to a FeatureSet slice
|
||||
var mergedFeatures []*licensetypes.Feature
|
||||
var mergedFeatures []*featuretypes.GettableFeature
|
||||
for _, feature := range featureMap {
|
||||
mergedFeatures = append(mergedFeatures, feature)
|
||||
}
|
||||
|
||||
@@ -3,79 +3,78 @@ package api
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMergeFeatureSets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
zeusFeatures []*licensetypes.Feature
|
||||
internalFeatures []*licensetypes.Feature
|
||||
expected []*licensetypes.Feature
|
||||
zeusFeatures []*featuretypes.GettableFeature
|
||||
internalFeatures []*featuretypes.GettableFeature
|
||||
expected []*featuretypes.GettableFeature
|
||||
}{
|
||||
{
|
||||
name: "empty zeusFeatures and internalFeatures",
|
||||
zeusFeatures: []*licensetypes.Feature{},
|
||||
internalFeatures: []*licensetypes.Feature{},
|
||||
expected: []*licensetypes.Feature{},
|
||||
zeusFeatures: []*featuretypes.GettableFeature{},
|
||||
internalFeatures: []*featuretypes.GettableFeature{},
|
||||
expected: []*featuretypes.GettableFeature{},
|
||||
},
|
||||
{
|
||||
name: "non-empty zeusFeatures and empty internalFeatures",
|
||||
zeusFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
zeusFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
},
|
||||
internalFeatures: []*licensetypes.Feature{},
|
||||
expected: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
internalFeatures: []*featuretypes.GettableFeature{},
|
||||
expected: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty zeusFeatures and non-empty internalFeatures",
|
||||
zeusFeatures: []*licensetypes.Feature{},
|
||||
internalFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
zeusFeatures: []*featuretypes.GettableFeature{},
|
||||
internalFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
},
|
||||
expected: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
expected: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
|
||||
zeusFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature3"), Active: false},
|
||||
zeusFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature3", Active: false},
|
||||
},
|
||||
internalFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature2"), Active: true},
|
||||
{Name: valuer.NewString("Feature4"), Active: false},
|
||||
internalFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature2", Active: true},
|
||||
{Name: "Feature4", Active: false},
|
||||
},
|
||||
expected: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: true},
|
||||
{Name: valuer.NewString("Feature3"), Active: false},
|
||||
{Name: valuer.NewString("Feature4"), Active: false},
|
||||
expected: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: true},
|
||||
{Name: "Feature3", Active: false},
|
||||
{Name: "Feature4", Active: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
|
||||
zeusFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
zeusFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
},
|
||||
internalFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: false},
|
||||
{Name: valuer.NewString("Feature3"), Active: true},
|
||||
internalFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: false},
|
||||
{Name: "Feature3", Active: true},
|
||||
},
|
||||
expected: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
{Name: valuer.NewString("Feature3"), Active: true},
|
||||
expected: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
{Name: "Feature3", Active: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const checkFirstTimeUser = useCallback((): boolean => {
|
||||
const users = usersData?.data || [];
|
||||
|
||||
const remainingUsers = (Array.isArray(users) ? users : []).filter(
|
||||
const remainingUsers = users.filter(
|
||||
(user) => user.email !== 'admin@signoz.cloud',
|
||||
);
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { Userpilot } from 'userpilot';
|
||||
import { extractDomain } from 'utils/app';
|
||||
|
||||
@@ -172,13 +171,11 @@ function App(): JSX.Element {
|
||||
user &&
|
||||
!!user.email
|
||||
) {
|
||||
// either the active API returns error with 404 or 501 and if it returns a terminated license means it's on basic plan
|
||||
const isOnBasicPlan =
|
||||
(activeLicenseFetchError &&
|
||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||
activeLicenseFetchError?.getHttpStatusCode(),
|
||||
)) ||
|
||||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
|
||||
activeLicenseFetchError &&
|
||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||
activeLicenseFetchError?.getHttpStatusCode(),
|
||||
);
|
||||
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
||||
|
||||
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
||||
@@ -193,10 +190,6 @@ function App(): JSX.Element {
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
(route) => route?.path !== ROUTES.BILLING,
|
||||
);
|
||||
|
||||
if (isEnterpriseSelfHostedUser) {
|
||||
updatedRoutes.push(LIST_LICENSES);
|
||||
}
|
||||
}
|
||||
// always add support route for cloud users
|
||||
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
||||
|
||||
27
frontend/src/api/dashboard/create.ts
Normal file
27
frontend/src/api/dashboard/create.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/create';
|
||||
|
||||
const createDashboard = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards';
|
||||
try {
|
||||
const response = await axios.post(url, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default createDashboard;
|
||||
9
frontend/src/api/dashboard/delete.ts
Normal file
9
frontend/src/api/dashboard/delete.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from 'api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/delete';
|
||||
|
||||
const deleteDashboard = (props: Props): Promise<PayloadProps> =>
|
||||
axios
|
||||
.delete<PayloadProps>(`/dashboards/${props.uuid}`)
|
||||
.then((response) => response.data);
|
||||
|
||||
export default deleteDashboard;
|
||||
11
frontend/src/api/dashboard/get.ts
Normal file
11
frontend/src/api/dashboard/get.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { ApiResponse } from 'types/api';
|
||||
import { Props } from 'types/api/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const getDashboard = (props: Props): Promise<Dashboard> =>
|
||||
axios
|
||||
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
|
||||
.then((res) => res.data.data);
|
||||
|
||||
export default getDashboard;
|
||||
8
frontend/src/api/dashboard/getAll.ts
Normal file
8
frontend/src/api/dashboard/getAll.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import axios from 'api';
|
||||
import { ApiResponse } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const getAllDashboardList = (): Promise<Dashboard[]> =>
|
||||
axios
|
||||
.get<ApiResponse<Dashboard[]>>('/dashboards')
|
||||
.then((res) => res.data.data);
|
||||
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
interface LockDashboardProps {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
|
||||
axios.put(`/dashboards/${props.uuid}/lock`);
|
||||
|
||||
export default lockDashboard;
|
||||
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
interface UnlockDashboardProps {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
|
||||
axios.put(`/dashboards/${props.uuid}/unlock`);
|
||||
|
||||
export default unlockDashboard;
|
||||
20
frontend/src/api/dashboard/update.ts
Normal file
20
frontend/src/api/dashboard/update.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||
|
||||
const updateDashboard = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const response = await axios.put(`/dashboards/${props.uuid}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateDashboard;
|
||||
10
frontend/src/api/features/getFeatureFlags.ts
Normal file
10
frontend/src/api/features/getFeatureFlags.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import axios from 'api';
|
||||
import { ApiResponse } from 'types/api';
|
||||
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
|
||||
|
||||
const getFeaturesFlags = (): Promise<FeatureFlagProps[]> =>
|
||||
axios
|
||||
.get<ApiResponse<FeatureFlagProps[]>>(`/featureFlags`)
|
||||
.then((response) => response.data.data);
|
||||
|
||||
export default getFeaturesFlags;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/create';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>('/dashboards', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
|
||||
|
||||
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>('/dashboards');
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
||||
@@ -1,21 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/delete';
|
||||
|
||||
const deleteDashboard = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteDashboard;
|
||||
@@ -1,20 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
|
||||
|
||||
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(
|
||||
`/dashboards/${props.id}/lock`,
|
||||
{ lock: props.lock },
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default lock;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
FeatureFlagProps,
|
||||
PayloadProps,
|
||||
} from 'types/api/features/getFeaturesFlags';
|
||||
|
||||
const list = async (): Promise<SuccessResponseV2<FeatureFlagProps[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/features`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default list;
|
||||
@@ -26,18 +26,6 @@ export interface UplotProps {
|
||||
resetScales?: boolean;
|
||||
}
|
||||
|
||||
function isAlignedData(data: unknown): data is uPlot.AlignedData {
|
||||
return Array.isArray(data) && data.length > 0;
|
||||
}
|
||||
|
||||
function isUplotOptions(options: unknown): options is uPlot.Options {
|
||||
return options !== null && typeof options === 'object';
|
||||
}
|
||||
|
||||
function isHTMLElement(el: unknown): el is HTMLElement {
|
||||
return el instanceof HTMLElement;
|
||||
}
|
||||
|
||||
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
(
|
||||
{ options, data, onDelete, onCreate, resetScales = true },
|
||||
@@ -90,19 +78,6 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!isUplotOptions(propOptionsRef.current) ||
|
||||
!isAlignedData(propDataRef.current) ||
|
||||
!isHTMLElement(targetRef.current)
|
||||
) {
|
||||
console.error('Uplot: Invalid options, data, or target element', {
|
||||
options: propOptionsRef.current,
|
||||
data: propDataRef.current,
|
||||
target: targetRef.current,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newChart = new UPlot(
|
||||
propOptionsRef.current,
|
||||
propDataRef.current,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// keep this consistent with backend plan.go
|
||||
// keep this consistent with backend constants.go
|
||||
export enum FeatureKeys {
|
||||
SSO = 'sso',
|
||||
USE_SPAN_METRICS = 'use_span_metrics',
|
||||
ONBOARDING = 'onboarding',
|
||||
CHAT_SUPPORT = 'chat_support',
|
||||
GATEWAY = 'gateway',
|
||||
PREMIUM_SUPPORT = 'premium_support',
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
ONBOARDING_V3 = 'onboarding_v3',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
SSO = 'SSO',
|
||||
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
|
||||
ONBOARDING = 'ONBOARDING',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
GATEWAY = 'GATEWAY',
|
||||
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
|
||||
ONBOARDING_V3 = 'ONBOARDING_V3',
|
||||
THIRD_PARTY_API = 'THIRD_PARTY_API',
|
||||
TRACE_FUNNELS = 'TRACE_FUNNELS',
|
||||
DOT_METRICS_ENABLED = 'DOT_METRICS_ENABLED',
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ describe('Create Alert Channel (Normal User)', () => {
|
||||
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO[vikrantgupta25]: check with Shaheer
|
||||
it.skip('Should check if the upgrade plan message is shown', () => {
|
||||
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
|
||||
expect(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { ExportPanelProps } from '.';
|
||||
import {
|
||||
@@ -34,28 +33,26 @@ function ExportPanelContainer({
|
||||
refetch,
|
||||
} = useGetAllDashboard();
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const {
|
||||
mutate: createNewDashboard,
|
||||
isLoading: createDashboardLoading,
|
||||
} = useMutation(createDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.data) {
|
||||
onExport(data?.data, true);
|
||||
if (data.payload) {
|
||||
onExport(data?.payload, true);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
|
||||
const options = useMemo(() => getSelectOptions(data || []), [data]);
|
||||
|
||||
const handleExportClick = useCallback((): void => {
|
||||
const currentSelectedDashboard = data?.data?.find(
|
||||
({ id }) => id === selectedDashboardId,
|
||||
const currentSelectedDashboard = data?.find(
|
||||
({ uuid }) => uuid === selectedDashboardId,
|
||||
);
|
||||
|
||||
onExport(currentSelectedDashboard || null, false);
|
||||
@@ -69,18 +66,14 @@ function ExportPanelContainer({
|
||||
);
|
||||
|
||||
const handleNewDashboard = useCallback(async () => {
|
||||
try {
|
||||
await createNewDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
}, [createNewDashboard, t, showErrorModal]);
|
||||
createNewDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
}, [t, createNewDashboard]);
|
||||
|
||||
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
|
||||
data.map(({ id, data }) => ({
|
||||
export const getSelectOptions = (
|
||||
data: AllDashboardsData,
|
||||
): SelectProps['options'] =>
|
||||
data.map(({ uuid, data }) => ({
|
||||
label: data.title,
|
||||
value: id,
|
||||
value: uuid,
|
||||
}));
|
||||
|
||||
export const filterOptions: SelectProps['filterOption'] = (
|
||||
|
||||
@@ -36,6 +36,7 @@ function QuerySection({
|
||||
const { t } = useTranslation('alerts');
|
||||
const [currentTab, setCurrentTab] = useState(queryCategory);
|
||||
|
||||
// TODO[vikrantgupta25] : check if this is still required ??
|
||||
const handleQueryCategoryChange = (queryType: string): void => {
|
||||
setQueryCategory(queryType as EQueryType);
|
||||
setCurrentTab(queryType as EQueryType);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from 'store';
|
||||
@@ -190,26 +189,24 @@ describe('WidgetGraphComponent', () => {
|
||||
it('should show correct menu items when hovering over more options while loading', async () => {
|
||||
const { getByTestId, findByRole, getByText, container } = render(
|
||||
<MockQueryClientProvider>
|
||||
<ErrorModalProvider>
|
||||
<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>
|
||||
</ErrorModalProvider>
|
||||
<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>,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Skeleton, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
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';
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -118,23 +119,29 @@ function WidgetGraphComponent({
|
||||
const updatedLayout =
|
||||
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
|
||||
|
||||
const updatedSelectedDashboard: Props = {
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
},
|
||||
id: selectedDashboard.id,
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
setDeleteModal(false);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -159,8 +166,7 @@ function WidgetGraphComponent({
|
||||
|
||||
updateDashboardMutation.mutateAsync(
|
||||
{
|
||||
id: selectedDashboard.id,
|
||||
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout,
|
||||
@@ -177,9 +183,9 @@ function WidgetGraphComponent({
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
notifications.success({
|
||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||
@@ -246,11 +252,7 @@ function WidgetGraphComponent({
|
||||
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData &&
|
||||
Array.isArray(widget.query.builder.queryData)
|
||||
? widget.query.builder.queryData
|
||||
: []
|
||||
).some(
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData ?? []).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
|
||||
@@ -160,6 +160,8 @@ function GridCardGraph({
|
||||
};
|
||||
});
|
||||
|
||||
// TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition
|
||||
// this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx
|
||||
useEffect(() => {
|
||||
if (variablesToGetUpdated.length > 0) {
|
||||
queryClient.cancelQueries([
|
||||
|
||||
@@ -31,9 +31,7 @@ export const getLocalStorageGraphVisibilityState = ({
|
||||
name: string;
|
||||
}): GraphVisibilityLegendEntryProps => {
|
||||
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||
graphVisibilityStates: Array(
|
||||
(Array.isArray(apiResponse) ? apiResponse.length : 0) + 1,
|
||||
).fill(true),
|
||||
graphVisibilityStates: Array(apiResponse.length + 1).fill(true),
|
||||
legendEntry: [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import { useForm } from 'antd/es/form/Form';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
@@ -13,6 +14,7 @@ import { DEFAULT_ROW_NAME } from 'container/NewDashboard/DashboardDescription/ut
|
||||
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 { defaultTo, isUndefined } from 'lodash-es';
|
||||
@@ -34,8 +36,7 @@ import { ItemCallback, Layout } from 'react-grid-layout';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -106,6 +107,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
||||
@@ -156,20 +158,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
logEvent('Dashboard Detail: Opened', {
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: data.uuid,
|
||||
dashboardName: data.title,
|
||||
numberOfPanels: data.widgets?.length,
|
||||
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [data, selectedDashboard?.id]);
|
||||
}, [data]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
const updatedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
panelMap: { ...currentPanelMap },
|
||||
@@ -184,18 +186,24 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
return widget;
|
||||
}),
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
setSelectedRowWidgetId(null);
|
||||
if (updatedDashboard.data) {
|
||||
if (updatedDashboard.data.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.data.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -278,25 +286,33 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
updatedWidgets?.push(currentWidget);
|
||||
|
||||
const updatedSelectedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
if (setPanelMap)
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
form.setFieldValue('title', '');
|
||||
setIsSettingsModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -399,14 +415,12 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
};
|
||||
|
||||
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
|
||||
if (oldItem?.i && currentPanelMap?.[oldItem.i]) {
|
||||
if (currentPanelMap[oldItem.i]) {
|
||||
const differenceY = newItem.y - oldItem.y;
|
||||
const widgetsInsideRow = (currentPanelMap[oldItem.i]?.widgets ?? []).map(
|
||||
(w) => ({
|
||||
...w,
|
||||
y: w.y + differenceY,
|
||||
}),
|
||||
);
|
||||
const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + differenceY,
|
||||
}));
|
||||
setCurrentPanelMap((prev) => ({
|
||||
...prev,
|
||||
[oldItem.i]: {
|
||||
@@ -433,26 +447,34 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
const updatedPanelMap = { ...currentPanelMap };
|
||||
delete updatedPanelMap[currentSelectRowId];
|
||||
|
||||
const updatedSelectedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
panelMap: updatedPanelMap,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
if (setPanelMap)
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
setIsDeleteModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
const isDashboardEmpty = useMemo(
|
||||
|
||||
@@ -102,11 +102,11 @@ export function updateStepInterval(
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query?.builder,
|
||||
...query.builder,
|
||||
queryData: [
|
||||
...(query?.builder?.queryData ?? []).map((queryData) => ({
|
||||
...query.builder.queryData.map((queryData) => ({
|
||||
...queryData,
|
||||
stepInterval: stepIntervalPoints || queryData?.stepInterval || 60,
|
||||
stepInterval: stepIntervalPoints || queryData.stepInterval || 60,
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -110,24 +110,24 @@ export function getQueryLegend(
|
||||
switch (currentQuery.queryType) {
|
||||
case EQueryType.QUERY_BUILDER:
|
||||
// check if the value is present in the queries
|
||||
legend = currentQuery?.builder?.queryData?.find(
|
||||
legend = currentQuery.builder.queryData.find(
|
||||
(query) => query.queryName === queryName,
|
||||
)?.legend;
|
||||
|
||||
if (!legend) {
|
||||
// check if the value is present in the formula
|
||||
legend = currentQuery?.builder?.queryFormulas?.find(
|
||||
legend = currentQuery.builder.queryFormulas.find(
|
||||
(query) => query.queryName === queryName,
|
||||
)?.legend;
|
||||
}
|
||||
break;
|
||||
case EQueryType.CLICKHOUSE:
|
||||
legend = currentQuery?.clickhouse_sql?.find(
|
||||
legend = currentQuery.clickhouse_sql.find(
|
||||
(query) => query.name === queryName,
|
||||
)?.legend;
|
||||
break;
|
||||
case EQueryType.PROM:
|
||||
legend = currentQuery?.promql?.find((query) => query.name === queryName)
|
||||
legend = currentQuery.promql.find((query) => query.name === queryName)
|
||||
?.legend;
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function Dashboards({
|
||||
useEffect(() => {
|
||||
if (!dashboardsList) return;
|
||||
|
||||
const sortedDashboards = dashboardsList.data.sort((a, b) => {
|
||||
const sortedDashboards = dashboardsList.sort((a, b) => {
|
||||
const aUpdateAt = new Date(a.updatedAt).getTime();
|
||||
const bUpdateAt = new Date(b.updatedAt).getTime();
|
||||
return bUpdateAt - aUpdateAt;
|
||||
@@ -103,7 +103,7 @@ export default function Dashboards({
|
||||
<div className="home-dashboards-list-container home-data-item-container">
|
||||
<div className="dashboards-list">
|
||||
{sortedDashboards.slice(0, 5).map((dashboard) => {
|
||||
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
|
||||
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.uuid}`;
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
@@ -134,7 +134,7 @@ export default function Dashboards({
|
||||
<div className="dashboard-item-name-container home-data-item-name-container">
|
||||
<img
|
||||
src={
|
||||
Math.random() % 2 === 0
|
||||
dashboard.id % 2 === 0
|
||||
? '/Icons/eight-ball.svg'
|
||||
: '/Icons/circus-tent.svg'
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const { Search } = Input;
|
||||
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
const { user } = useAppContext();
|
||||
// TODO[vikrantgupta25]: check with sagar on cleanup
|
||||
const [addNewAlert, action] = useComponentPermission(
|
||||
['add_new_alert', 'action'],
|
||||
user.role,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from 'antd';
|
||||
import { TableProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
@@ -63,7 +63,6 @@ import {
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
ChangeEvent,
|
||||
@@ -84,7 +83,6 @@ import {
|
||||
WidgetRow,
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||
import ImportJSON from './ImportJSON';
|
||||
@@ -228,7 +226,7 @@ function DashboardsList(): JSX.Element {
|
||||
useEffect(() => {
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchString,
|
||||
dashboardListResponse?.data || [],
|
||||
dashboardListResponse || [],
|
||||
);
|
||||
if (sortOrder.columnKey === 'updatedAt') {
|
||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||
@@ -258,19 +256,17 @@ function DashboardsList(): JSX.Element {
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const data: Data[] =
|
||||
dashboards?.map((e) => ({
|
||||
createdAt: e.createdAt,
|
||||
description: e.data.description || '',
|
||||
id: e.id,
|
||||
id: e.uuid,
|
||||
lastUpdatedTime: e.updatedAt,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.id,
|
||||
key: e.uuid,
|
||||
createdBy: e.createdBy,
|
||||
isLocked: !!e.locked || false,
|
||||
isLocked: !!e.isLocked || false,
|
||||
lastUpdatedBy: e.updatedBy,
|
||||
image: e.data.image || Base64Icons[0],
|
||||
variables: e.data.variables,
|
||||
@@ -296,20 +292,28 @@ function DashboardsList(): JSX.Element {
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
);
|
||||
if (response.statusCode === 200) {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
}, [newDashboardState, safeNavigate, showErrorModal, t]);
|
||||
}, [newDashboardState, safeNavigate, t]);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
logEvent('Dashboard List: Import JSON clicked', {});
|
||||
@@ -323,7 +327,7 @@ function DashboardsList(): JSX.Element {
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchText,
|
||||
dashboardListResponse?.data || [],
|
||||
dashboardListResponse || [],
|
||||
);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
@@ -673,7 +677,7 @@ function DashboardsList(): JSX.Element {
|
||||
!isUndefined(dashboardListResponse)
|
||||
) {
|
||||
logEvent('Dashboard List: Page visited', {
|
||||
number: dashboardListResponse?.data?.length,
|
||||
number: dashboardListResponse?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
|
||||
@@ -14,21 +14,19 @@ import {
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
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 { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
// #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
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function ImportJSON({
|
||||
isImportJSONModalVisible,
|
||||
@@ -76,8 +74,6 @@ function ImportJSON({
|
||||
}
|
||||
};
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const onClickLoadJsonHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setDashboardCreating(true);
|
||||
@@ -85,6 +81,11 @@ function ImportJSON({
|
||||
|
||||
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
||||
|
||||
// Remove uuid from the dashboard data, in all cases - empty, duplicate or any valid not duplicate uuid
|
||||
if (dashboardData.uuid !== undefined) {
|
||||
delete dashboardData.uuid;
|
||||
}
|
||||
|
||||
if (dashboardData?.layout) {
|
||||
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
|
||||
} else {
|
||||
@@ -96,19 +97,28 @@ function ImportJSON({
|
||||
uploadedGrafana,
|
||||
});
|
||||
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
);
|
||||
logEvent('Dashboard List: New dashboard imported successfully', {
|
||||
dashboardId: response.data?.id,
|
||||
dashboardName: response.data?.data?.title,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
logEvent('Dashboard List: New dashboard imported successfully', {
|
||||
dashboardId: response.payload?.uuid,
|
||||
dashboardName: response.payload?.data?.title,
|
||||
});
|
||||
} else {
|
||||
setIsCreateDashboardError(true);
|
||||
notifications.error({
|
||||
message:
|
||||
response.error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
setDashboardCreating(false);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setDashboardCreating(false);
|
||||
setIsCreateDashboardError(true);
|
||||
notifications.error({
|
||||
|
||||
@@ -6,7 +6,8 @@ import { executeSearchQueries } from '../utils';
|
||||
|
||||
describe('executeSearchQueries', () => {
|
||||
const firstDashboard: Dashboard = {
|
||||
id: uuid(),
|
||||
id: 11111,
|
||||
uuid: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
@@ -17,7 +18,8 @@ describe('executeSearchQueries', () => {
|
||||
},
|
||||
};
|
||||
const secondDashboard: Dashboard = {
|
||||
id: uuid(),
|
||||
id: 22222,
|
||||
uuid: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
@@ -28,7 +30,8 @@ describe('executeSearchQueries', () => {
|
||||
},
|
||||
};
|
||||
const thirdDashboard: Dashboard = {
|
||||
id: uuid(),
|
||||
id: 333333,
|
||||
uuid: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
|
||||
@@ -59,7 +59,7 @@ export function DeleteButton({
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
deleteDashboardMutation.mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: t('dashboard:delete_dashboard_success', {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const generateSearchData = (
|
||||
|
||||
dashboards.forEach((dashboard) => {
|
||||
dashboardSearchData.push({
|
||||
id: dashboard.id,
|
||||
id: dashboard.uuid,
|
||||
title: dashboard.data.title,
|
||||
description: dashboard.data.description,
|
||||
tags: dashboard.data.tags || [],
|
||||
|
||||
@@ -280,7 +280,7 @@ export const parseFieldValue = (value: string): string => {
|
||||
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
|
||||
// the log line readable
|
||||
export const removeEscapeCharacters = (str: string): string =>
|
||||
(str ?? '')
|
||||
str
|
||||
.replace(/\\x1[bB][[0-9;]*m/g, '')
|
||||
.replace(/\\u001[bB][[0-9;]*m/g, '')
|
||||
.replace(/\\x[0-9A-Fa-f]{2}/g, '')
|
||||
@@ -292,7 +292,7 @@ export const removeEscapeCharacters = (str: string): string =>
|
||||
//
|
||||
// so we need to remove this escapes to render the color properly
|
||||
export const unescapeString = (str: string): string =>
|
||||
(str ?? '')
|
||||
str
|
||||
.replace(/\\n/g, '\n') // Replaces escaped newlines
|
||||
.replace(/\\r/g, '\r') // Replaces escaped carriage returns
|
||||
.replace(/\\t/g, '\t') // Replaces escaped tabs
|
||||
|
||||
@@ -62,8 +62,6 @@ function LogsExplorerChart({
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
// Remove Hidden Filters from URL query parameters on time change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
},
|
||||
|
||||
@@ -28,12 +28,16 @@ import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import dayjs from 'dayjs';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
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';
|
||||
@@ -94,6 +98,7 @@ function LogsExplorerViews({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
chartQueryKeyRef: MutableRefObject<any>;
|
||||
}): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
@@ -136,6 +141,8 @@ function LogsExplorerViews({
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
|
||||
const handleAxisError = useAxiosError();
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
@@ -188,26 +195,6 @@ function LogsExplorerViews({
|
||||
},
|
||||
],
|
||||
legend: '{{severity_text}}',
|
||||
...(activeLogId && {
|
||||
filters: {
|
||||
...listQuery?.filters,
|
||||
items: [
|
||||
...(listQuery?.filters?.items || []),
|
||||
{
|
||||
id: v4(),
|
||||
key: {
|
||||
key: 'id',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['<='],
|
||||
value: activeLogId,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const modifiedQuery: Query = {
|
||||
@@ -222,7 +209,7 @@ function LogsExplorerViews({
|
||||
};
|
||||
|
||||
return modifiedQuery;
|
||||
}, [stagedQuery, listQuery, activeLogId]);
|
||||
}, [stagedQuery, listQuery]);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
@@ -307,12 +294,12 @@ function LogsExplorerViews({
|
||||
});
|
||||
|
||||
// Add filter for activeLogId if present
|
||||
let updatedFilters = params.filters;
|
||||
let updatedFilters = paginateData.filters;
|
||||
if (activeLogId) {
|
||||
updatedFilters = {
|
||||
...params.filters,
|
||||
...paginateData.filters,
|
||||
items: [
|
||||
...(params.filters?.items || []),
|
||||
...(paginateData.filters?.items || []),
|
||||
{
|
||||
id: v4(),
|
||||
key: {
|
||||
@@ -409,6 +396,11 @@ function LogsExplorerViews({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload]);
|
||||
|
||||
const {
|
||||
mutate: updateDashboard,
|
||||
isLoading: isUpdateDashboardLoading,
|
||||
} = useUpdateDashboard();
|
||||
|
||||
const getUpdatedQueryForExport = useCallback((): Query => {
|
||||
const updatedQuery = cloneDeep(currentQuery);
|
||||
|
||||
@@ -432,22 +424,68 @@ function LogsExplorerViews({
|
||||
? getUpdatedQueryForExport()
|
||||
: exportDefaultQuery;
|
||||
|
||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||
dashboard,
|
||||
query,
|
||||
widgetId,
|
||||
panelTypeParam,
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
isNewDashboard,
|
||||
dashboardName: dashboard?.data?.title,
|
||||
});
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
const message =
|
||||
data.error === 'feature usage exceeded' ? (
|
||||
<span>
|
||||
Panel limit exceeded for {DataSource.LOGS} in community edition. Please
|
||||
checkout our paid plans{' '}
|
||||
<a
|
||||
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
data.error
|
||||
);
|
||||
notifications.error({
|
||||
message,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: data.payload?.uuid || '',
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
onError: handleAxisError,
|
||||
});
|
||||
},
|
||||
[getUpdatedQueryForExport, exportDefaultQuery, safeNavigate, panelType],
|
||||
[
|
||||
getUpdatedQueryForExport,
|
||||
exportDefaultQuery,
|
||||
options.selectColumns,
|
||||
safeNavigate,
|
||||
notifications,
|
||||
panelType,
|
||||
updateDashboard,
|
||||
handleAxisError,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -773,6 +811,7 @@ function LogsExplorerViews({
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isUpdateDashboardLoading}
|
||||
onExport={handleExport}
|
||||
sourcepage={DataSource.LOGS}
|
||||
/>
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import LogsExplorerViews from '..';
|
||||
import {
|
||||
logsQueryRangeSuccessNewFormatResponse,
|
||||
mockQueryBuilderContextValue,
|
||||
} from './mock';
|
||||
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
|
||||
|
||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||
const ACTIVE_LOG_ID = 'test-log-id';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
@@ -87,12 +81,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/logs/useCopyLogLink', () => ({
|
||||
useCopyLogLink: jest.fn().mockReturnValue({
|
||||
activeLogId: ACTIVE_LOG_ID,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
|
||||
beforeEach(() => {
|
||||
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||
@@ -174,47 +162,4 @@ describe('LogsExplorerViews -', () => {
|
||||
queryByText('Something went wrong. Please try again or contact support.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add activeLogId filter when present in URL', () => {
|
||||
// Mock useCopyLogLink to return an activeLogId
|
||||
(useCopyLogLink as jest.Mock).mockReturnValue({
|
||||
activeLogId: ACTIVE_LOG_ID,
|
||||
});
|
||||
|
||||
lodsQueryServerRequest();
|
||||
render(
|
||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
// Get the query data from the first call to useGetExplorerQueryRange
|
||||
const {
|
||||
queryData,
|
||||
} = (useGetExplorerQueryRange as jest.Mock).mock.calls[0][0].builder;
|
||||
const firstQuery = queryData[0];
|
||||
|
||||
// Get the original number of filters from mock data
|
||||
const originalFiltersLength =
|
||||
mockQueryBuilderContextValue.currentQuery.builder.queryData[0].filters?.items
|
||||
.length || 0;
|
||||
const expectedFiltersLength = originalFiltersLength + 1; // +1 for activeLogId filter
|
||||
|
||||
// Verify that the activeLogId filter is present
|
||||
expect(
|
||||
firstQuery.filters?.items.some(
|
||||
(item: TagFilterItem) =>
|
||||
item.key?.key === 'id' && item.op === '<=' && item.value === ACTIVE_LOG_ID,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// Verify the total number of filters (original + 1 new activeLogId filter)
|
||||
expect(firstQuery.filters?.items.length).toBe(expectedFiltersLength);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
OPERATORS,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { noop } from 'lodash-es';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const logsQueryRangeSuccessNewFormatResponse = {
|
||||
data: {
|
||||
result: [],
|
||||
@@ -59,148 +49,3 @@ export const logsQueryRangeSuccessNewFormatResponse = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockQueryBuilderContextValue = {
|
||||
isDefaultQuery: (): boolean => false,
|
||||
currentQuery: {
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
key: 'service',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: {
|
||||
key: 'log_level',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'INFO',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
initialQueryBuilderFormValues,
|
||||
],
|
||||
},
|
||||
},
|
||||
setSupersetQuery: jest.fn(),
|
||||
supersetQuery: {
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
key: 'service',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: {
|
||||
key: 'log_level',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'INFO',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
initialQueryBuilderFormValues,
|
||||
],
|
||||
},
|
||||
},
|
||||
stagedQuery: {
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
key: 'service',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: {
|
||||
key: 'log_level',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'INFO',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
initialQueryBuilderFormValues,
|
||||
],
|
||||
},
|
||||
},
|
||||
initialDataSource: null,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isEnabledQuery: false,
|
||||
lastUsedQuery: 0,
|
||||
setLastUsedQuery: noop,
|
||||
handleSetQueryData: noop,
|
||||
handleSetFormulaData: noop,
|
||||
handleSetQueryItemData: noop,
|
||||
handleSetConfig: noop,
|
||||
removeQueryBuilderEntityByIndex: noop,
|
||||
removeQueryTypeItemByIndex: noop,
|
||||
addNewBuilderQuery: noop,
|
||||
cloneQuery: noop,
|
||||
addNewFormula: noop,
|
||||
addNewQueryItem: noop,
|
||||
redirectWithQueryBuilderData: noop,
|
||||
handleRunQuery: noop,
|
||||
resetQuery: noop,
|
||||
updateAllQueriesOperators: (): Query => initialQueriesMap.logs,
|
||||
updateQueriesData: (): Query => initialQueriesMap.logs,
|
||||
initQueryBuilderData: noop,
|
||||
handleOnUnitsChange: noop,
|
||||
isStagedQueryUpdated: (): boolean => false,
|
||||
};
|
||||
|
||||
@@ -2,12 +2,18 @@ import './Explorer.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@@ -33,6 +39,13 @@ function Explorer(): JSX.Element {
|
||||
currentQuery,
|
||||
} = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { notifications } = useNotifications();
|
||||
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.METRICS_LIST_OPTIONS,
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateOperator: 'noop',
|
||||
});
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isOneChartPerQueryEnabled =
|
||||
@@ -73,16 +86,59 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const widgetId = uuid();
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
dashboardId: dashboard.id,
|
||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||
dashboard,
|
||||
queryToExport || exportDefaultQuery,
|
||||
widgetId,
|
||||
});
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
const message =
|
||||
data.error === 'feature usage exceeded' ? (
|
||||
<span>
|
||||
Panel limit exceeded for {DataSource.METRICS} in community edition.
|
||||
Please checkout our paid plans{' '}
|
||||
<a
|
||||
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
data.error
|
||||
);
|
||||
notifications.error({
|
||||
message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
dashboardId: data.payload?.uuid || '',
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
notifications.error({
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[exportDefaultQuery, safeNavigate],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[exportDefaultQuery, notifications, updateDashboard],
|
||||
);
|
||||
|
||||
const splitedQueries = useMemo(
|
||||
@@ -145,6 +201,7 @@ function Explorer(): JSX.Element {
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
sourcepage={DataSource.METRICS}
|
||||
onExport={handleExport}
|
||||
isOneChartPerQuery={showOneChartPerQuery}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -43,8 +44,11 @@ import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import {
|
||||
Dashboard,
|
||||
DashboardData,
|
||||
IDashboardVariable,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -61,9 +65,10 @@ interface DashboardDescriptionProps {
|
||||
|
||||
export function sanitizeDashboardData(
|
||||
selectedData: DashboardData,
|
||||
): DashboardData {
|
||||
): Omit<DashboardData, 'uuid'> {
|
||||
if (!selectedData?.variables) {
|
||||
return selectedData;
|
||||
const { uuid, ...rest } = selectedData;
|
||||
return rest;
|
||||
}
|
||||
|
||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||
@@ -75,8 +80,9 @@ export function sanitizeDashboardData(
|
||||
{} as Record<string, IDashboardVariable>,
|
||||
);
|
||||
|
||||
const { uuid, ...restData } = selectedData;
|
||||
return {
|
||||
...selectedData,
|
||||
...restData,
|
||||
variables: updatedVariables,
|
||||
};
|
||||
}
|
||||
@@ -102,7 +108,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
const selectedData = selectedDashboard
|
||||
? {
|
||||
...selectedDashboard.data,
|
||||
uuid: selectedDashboard.id,
|
||||
uuid: selectedDashboard.uuid,
|
||||
}
|
||||
: ({} as DashboardData);
|
||||
|
||||
@@ -156,7 +162,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||
});
|
||||
@@ -172,9 +178,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
const updatedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
|
||||
const updatedDashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
title: updatedTitle,
|
||||
@@ -186,9 +191,13 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
message: 'Dashboard renamed successfully',
|
||||
});
|
||||
setIsRenameDashboardOpen(false);
|
||||
if (updatedDashboard.data) setSelectedDashboard(updatedDashboard.data);
|
||||
if (updatedDashboard.payload)
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
setIsRenameDashboardOpen(true);
|
||||
},
|
||||
});
|
||||
@@ -242,9 +251,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const updatedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout: [
|
||||
@@ -271,21 +279,28 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
},
|
||||
],
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.data) {
|
||||
if (updatedDashboard.data.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.data.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
}
|
||||
|
||||
setIsPanelNameModalOpen(false);
|
||||
setSectionName(DEFAULT_ROW_NAME);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -430,7 +445,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
<DeleteButton
|
||||
createdBy={selectedDashboard?.createdBy || ''}
|
||||
name={selectedDashboard?.data.title || ''}
|
||||
id={String(selectedDashboard?.id) || ''}
|
||||
id={String(selectedDashboard?.uuid) || ''}
|
||||
isLocked={isDashboardLocked}
|
||||
routeToListPage
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import './GeneralSettings.styles.scss';
|
||||
|
||||
import { Col, Input, Select, Space, Typography } from 'antd';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
@@ -36,12 +38,14 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
updateDashboardMutation.mutate(
|
||||
updateDashboardMutation.mutateAsync(
|
||||
{
|
||||
id: selectedDashboard.id,
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
description: updatedDescription,
|
||||
@@ -52,11 +56,15 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
if (updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
},
|
||||
onError: () => {},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -185,7 +185,7 @@ function VariableItem({
|
||||
|
||||
if (details.error) {
|
||||
let message = details.error;
|
||||
if ((details.error ?? '').toString().includes('Syntax error:')) {
|
||||
if (details.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
|
||||
@@ -171,8 +171,7 @@ function VariablesSetting({
|
||||
|
||||
updateMutation.mutateAsync(
|
||||
{
|
||||
id: selectedDashboard.id,
|
||||
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
variables: updatedVariablesData,
|
||||
@@ -180,13 +179,18 @@ function VariablesSetting({
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
if (updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
notifications.success({
|
||||
message: t('variable_updated_successfully'),
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: t('error_while_updating_variable'),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,14 +116,14 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
const oldVariables = prev?.data.variables;
|
||||
// this is added to handle case where we have two different
|
||||
// schemas for variable response
|
||||
if (oldVariables?.[id]) {
|
||||
if (oldVariables[id]) {
|
||||
oldVariables[id] = {
|
||||
...oldVariables[id],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
};
|
||||
}
|
||||
if (oldVariables?.[name]) {
|
||||
if (oldVariables[name]) {
|
||||
oldVariables[name] = {
|
||||
...oldVariables[name],
|
||||
selectedValue: value,
|
||||
|
||||
@@ -229,7 +229,7 @@ function VariableItem({
|
||||
|
||||
if (details.error) {
|
||||
let message = details.error;
|
||||
if ((details.error ?? '').toString().includes('Syntax error:')) {
|
||||
if (details.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
|
||||
@@ -117,9 +117,6 @@ export const buildParentDependencyGraph = (
|
||||
Object.entries(graph).forEach(([node, children]) => {
|
||||
// For each child, add the current node as its parent
|
||||
children.forEach((child) => {
|
||||
if (!parentGraph[child]) {
|
||||
parentGraph[child] = [];
|
||||
}
|
||||
parentGraph[child].push(node);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ function QuerySection({
|
||||
panelType: selectedWidget.panelTypes,
|
||||
queryType: currentQuery.queryType,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
isNewPanel,
|
||||
});
|
||||
|
||||
@@ -125,11 +125,7 @@ function WidgetGraph({
|
||||
// context redirection to explorer pages
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef,
|
||||
isButtonEnabled: (selectedWidget?.query?.builder?.queryData &&
|
||||
Array.isArray(selectedWidget.query.builder.queryData)
|
||||
? selectedWidget.query.builder.queryData
|
||||
: []
|
||||
).some(
|
||||
isButtonEnabled: (selectedWidget?.query?.builder?.queryData ?? []).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
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';
|
||||
@@ -40,10 +41,10 @@ import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
ColumnUnit,
|
||||
Dashboard,
|
||||
LegendPosition,
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -140,11 +141,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
if (!logEventCalledRef.current) {
|
||||
logEvent('Panel Edit: Page visited', {
|
||||
panelType: selectedWidget?.panelTypes,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: selectedWidget?.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
isNewPanel: !!isWidgetNotPresent,
|
||||
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
|
||||
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
@@ -344,6 +345,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
return { selectedWidget, preWidgets, afterWidgets };
|
||||
}, [selectedDashboard, query]);
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
// this loading state is to take care of mismatch in the responses for table and other panels
|
||||
// hence while changing the query contains the older value and the processing logic fails
|
||||
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
||||
@@ -372,7 +375,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: updatedQuery?.builder?.queryData?.[0]?.limit || 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -467,9 +470,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
updatedLayout = newLayoutItem;
|
||||
}
|
||||
|
||||
const dashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
|
||||
const dashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
uuid: selectedDashboard.uuid,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: isNewDashboard
|
||||
@@ -537,14 +540,15 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(dashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
onSuccess: () => {
|
||||
setSelectedRowWidgetId(null);
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
setSelectedDashboard(dashboard);
|
||||
setToScrollWidgetId(selectedWidget?.id || '');
|
||||
safeNavigate({
|
||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
||||
});
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
}, [
|
||||
selectedDashboard,
|
||||
@@ -558,6 +562,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
currentQuery,
|
||||
preWidgets,
|
||||
updateDashboardMutation,
|
||||
handleError,
|
||||
widgets,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
@@ -596,12 +601,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
logEvent('Panel Edit: Save changes', {
|
||||
panelType: selectedWidget.panelTypes,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
queryType: currentQuery.queryType,
|
||||
isNewPanel: isUndefined(selectWidget),
|
||||
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
|
||||
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
|
||||
});
|
||||
setSaveModal(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -609,7 +614,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
const isNewTraceLogsAvailable =
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
currentQuery?.builder?.queryData?.find(
|
||||
currentQuery.builder.queryData.find(
|
||||
(query) => query.dataSource !== DataSource.METRICS,
|
||||
) !== undefined;
|
||||
|
||||
@@ -620,7 +625,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
}
|
||||
|
||||
const isTraceOrLogsQueryBuilder =
|
||||
currentQuery?.builder?.queryData?.find(
|
||||
currentQuery.builder.queryData.find(
|
||||
(query) =>
|
||||
query.dataSource === DataSource.TRACES ||
|
||||
query.dataSource === DataSource.LOGS,
|
||||
@@ -632,7 +637,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
return isNewTraceLogsAvailable;
|
||||
}, [
|
||||
currentQuery?.builder?.queryData,
|
||||
currentQuery.builder.queryData,
|
||||
selectedWidget?.id,
|
||||
isNewTraceLogsAvailable,
|
||||
]);
|
||||
@@ -661,7 +666,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGraph === PANEL_TYPES.LIST) {
|
||||
const initialDataSource = currentQuery?.builder?.queryData?.[0]?.dataSource;
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
if (initialDataSource === DataSource.LOGS) {
|
||||
// we do not need selected log columns in the request data as the entire response contains all the necessary data
|
||||
setRequestData((prev) => ({
|
||||
|
||||
@@ -55,11 +55,7 @@ function HistogramPanelWrapper({
|
||||
if (setGraphVisibility) {
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [
|
||||
queryResponse?.data?.payload?.data?.result,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
]);
|
||||
}, [queryResponse.data?.payload.data.result, setGraphVisibility, widget.id]);
|
||||
|
||||
const histogramOptions = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -54,7 +54,7 @@ function PiePanelWrapper({
|
||||
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
|
||||
return {
|
||||
label,
|
||||
value: d?.values?.[0]?.[1],
|
||||
value: d.values?.[0]?.[1],
|
||||
color:
|
||||
widget?.customLegendColors?.[label] ||
|
||||
generateColor(
|
||||
|
||||
@@ -75,11 +75,7 @@ function UplotPanelWrapper({
|
||||
if (setGraphVisibility) {
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [
|
||||
queryResponse?.data?.payload?.data?.result,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
]);
|
||||
}, [queryResponse.data?.payload.data.result, setGraphVisibility, widget.id]);
|
||||
|
||||
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
|
||||
@@ -73,7 +73,7 @@ function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const queryData = (currentQuery?.builder?.queryData || [])?.find(
|
||||
const queryData = currentQuery.builder.queryData.find(
|
||||
(item) => item.queryName === queryName,
|
||||
);
|
||||
const filters = queryData?.filters?.items;
|
||||
|
||||
@@ -26,7 +26,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
@@ -302,11 +301,10 @@ function SideNav(): JSX.Element {
|
||||
}
|
||||
|
||||
const isOnBasicPlan =
|
||||
(activeLicenseFetchError &&
|
||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||
activeLicenseFetchError?.getHttpStatusCode(),
|
||||
)) ||
|
||||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
|
||||
activeLicenseFetchError &&
|
||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||
activeLicenseFetchError?.getHttpStatusCode(),
|
||||
);
|
||||
|
||||
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
|
||||
updatedMenuItems = updatedMenuItems.filter(
|
||||
@@ -355,7 +353,6 @@ function SideNav(): JSX.Element {
|
||||
t,
|
||||
user.role,
|
||||
activeLicenseFetchError,
|
||||
activeLicense?.status,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -377,8 +377,6 @@ function DateTimeSelection({
|
||||
urlQuery.delete('endTime');
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, value);
|
||||
// Remove Hidden Filters from URL query parameters on time change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
@@ -671,7 +669,9 @@ function DateTimeSelection({
|
||||
urlQuery.set(QueryParams.endTime, endTime);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
}
|
||||
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
|
||||
safeNavigate(generatedUrl);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, updateTimeInterval, globalTimeLoading]);
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getChartData = (
|
||||
};
|
||||
const chartLabels: ChartData<'line'>['labels'] = [];
|
||||
|
||||
Object.keys(allDataPoints ?? {}).forEach((timestamp) => {
|
||||
Object.keys(allDataPoints).forEach((timestamp) => {
|
||||
const key = allDataPoints[timestamp];
|
||||
if (key.value) {
|
||||
chartDataset.data.push(key.value);
|
||||
|
||||
@@ -149,30 +149,28 @@ function SpanOverview({
|
||||
<Typography.Text className="service-name">
|
||||
{span.serviceName}
|
||||
</Typography.Text>
|
||||
{!!span.serviceName &&
|
||||
!!span.name &&
|
||||
process.env.NODE_ENV === 'development' && (
|
||||
<div className="add-funnel-button">
|
||||
<span className="add-funnel-button__separator">·</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="add-funnel-button__button"
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleAddSpanToFunnel(span);
|
||||
}}
|
||||
icon={
|
||||
<img
|
||||
className="add-funnel-button__icon"
|
||||
src="/Icons/funnel-add.svg"
|
||||
alt="funnel-icon"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!!span.serviceName && !!span.name && (
|
||||
<div className="add-funnel-button">
|
||||
<span className="add-funnel-button__separator">·</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="add-funnel-button__button"
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleAddSpanToFunnel(span);
|
||||
}}
|
||||
icon={
|
||||
<img
|
||||
className="add-funnel-button__icon"
|
||||
src="/Icons/funnel-add.svg"
|
||||
alt="funnel-icon"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -450,7 +448,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
virtualiserRef={virtualizerRef}
|
||||
setColumnWidths={setTraceFlamegraphStatsWidth}
|
||||
/>
|
||||
{selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
|
||||
{selectedSpanToAddToFunnel && (
|
||||
<AddSpanToFunnelModal
|
||||
span={selectedSpanToAddToFunnel}
|
||||
isOpen={isAddSpanToFunnelModalOpen}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import deleteDashboard from 'api/v1/dashboards/id/delete';
|
||||
import deleteDashboard from 'api/dashboard/delete';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { PayloadProps } from 'types/api/dashboard/delete';
|
||||
|
||||
export const useDeleteDashboard = (
|
||||
id: string,
|
||||
): UseMutationResult<SuccessResponseV2<null>, APIError, void, unknown> => {
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
return useMutation<SuccessResponseV2<null>, APIError>({
|
||||
): UseMutationResult<PayloadProps, unknown, void, unknown> =>
|
||||
useMutation({
|
||||
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
|
||||
mutationFn: () =>
|
||||
deleteDashboard({
|
||||
id,
|
||||
uuid: id,
|
||||
}),
|
||||
onError: (error: APIError) => {
|
||||
showErrorModal(error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import getAll from 'api/v1/dashboards/getAll';
|
||||
import { getAllDashboardList } from 'api/dashboard/getAll';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export const useGetAllDashboard = (): UseQueryResult<
|
||||
SuccessResponseV2<Dashboard[]>,
|
||||
APIError
|
||||
> => {
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
return useQuery<SuccessResponseV2<Dashboard[]>, APIError>({
|
||||
queryFn: getAll,
|
||||
onError: (error) => {
|
||||
showErrorModal(error);
|
||||
},
|
||||
export const useGetAllDashboard = (): UseQueryResult<Dashboard[], unknown> =>
|
||||
useQuery<Dashboard[]>({
|
||||
queryFn: getAllDashboardList,
|
||||
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import update from 'api/v1/dashboards/id/update';
|
||||
import update from 'api/dashboard/update';
|
||||
import dayjs from 'dayjs';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export const useUpdateDashboard = (): UseUpdateDashboard => {
|
||||
const { updatedTimeRef } = useDashboard();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
return useMutation(update, {
|
||||
onSuccess: (data) => {
|
||||
if (data.data) {
|
||||
updatedTimeRef.current = dayjs(data.data.updatedAt);
|
||||
if (data.payload) {
|
||||
updatedTimeRef.current = dayjs(data.payload.updatedAt);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorModal(error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type UseUpdateDashboard = UseMutationResult<
|
||||
SuccessResponseV2<Dashboard>,
|
||||
APIError,
|
||||
SuccessResponse<Dashboard> | ErrorResponse,
|
||||
unknown,
|
||||
Props,
|
||||
unknown
|
||||
>;
|
||||
|
||||
@@ -38,7 +38,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
logEvent('Panel Edit: Create alert', {
|
||||
panelType: widget.panelTypes,
|
||||
dashboardName: selectedDashboard?.data?.title,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: widget.id,
|
||||
queryType: widget.query.queryType,
|
||||
});
|
||||
@@ -47,7 +47,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
action: MenuItemKeys.CreateAlerts,
|
||||
panelType: widget.panelTypes,
|
||||
dashboardName: selectedDashboard?.data?.title,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
widgetId: widget.id,
|
||||
queryType: widget.query.queryType,
|
||||
});
|
||||
|
||||
@@ -49,12 +49,12 @@ export const useGetQueryRange: UseGetQueryRange = (
|
||||
...requestData.query.builder,
|
||||
queryData: [
|
||||
{
|
||||
...requestData?.query?.builder?.queryData[0],
|
||||
...requestData.query.builder.queryData[0],
|
||||
orderBy: [
|
||||
...(requestData?.query?.builder?.queryData[0]?.orderBy || []),
|
||||
...requestData.query.builder.queryData[0].orderBy,
|
||||
{
|
||||
columnName: 'id',
|
||||
order: requestData?.query?.builder?.queryData[0]?.orderBy[0]?.order,
|
||||
order: requestData.query.builder.queryData[0].orderBy[0].order,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import list from 'api/v1/features/list';
|
||||
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
|
||||
|
||||
export interface Props {
|
||||
onSuccessHandler: (routes: FeatureFlagProps[]) => void;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
type UseGetFeatureFlag = UseQueryResult<
|
||||
SuccessResponseV2<FeatureFlagProps[]>,
|
||||
APIError
|
||||
>;
|
||||
|
||||
export const useGetFeatureFlag = (
|
||||
const useGetFeatureFlag = (
|
||||
onSuccessHandler: (routes: FeatureFlagProps[]) => void,
|
||||
isLoggedIn: boolean,
|
||||
): UseGetFeatureFlag =>
|
||||
useQuery<SuccessResponseV2<FeatureFlagProps[]>, APIError>({
|
||||
): UseQueryResult<FeatureFlagProps[], unknown> =>
|
||||
useQuery<FeatureFlagProps[]>({
|
||||
queryFn: getFeaturesFlags,
|
||||
queryKey: [REACT_QUERY_KEY.GET_FEATURES_FLAGS],
|
||||
queryFn: () => list(),
|
||||
onSuccess: (data) => {
|
||||
onSuccessHandler(data.data);
|
||||
},
|
||||
onSuccess: onSuccessHandler,
|
||||
retryOnMount: false,
|
||||
enabled: !!isLoggedIn,
|
||||
});
|
||||
|
||||
export default useGetFeatureFlag;
|
||||
|
||||
@@ -432,19 +432,18 @@ export const getUPlotChartOptions = ({
|
||||
const globalCleanupHandler = (e: MouseEvent): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target &&
|
||||
!target?.closest?.('.u-legend') &&
|
||||
!target?.classList?.contains?.('legend-tooltip')
|
||||
!target.closest('.u-legend') &&
|
||||
!target.classList.contains('legend-tooltip')
|
||||
) {
|
||||
cleanupAllTooltips();
|
||||
}
|
||||
};
|
||||
document?.addEventListener('mousemove', globalCleanupHandler);
|
||||
document.addEventListener('mousemove', globalCleanupHandler);
|
||||
|
||||
// Store cleanup function for potential removal later
|
||||
(self as any)._tooltipCleanup = (): void => {
|
||||
cleanupAllTooltips();
|
||||
document?.removeEventListener('mousemove', globalCleanupHandler);
|
||||
document.removeEventListener('mousemove', globalCleanupHandler);
|
||||
};
|
||||
|
||||
const seriesEls = legend.querySelectorAll('.u-series');
|
||||
@@ -485,7 +484,7 @@ export const getUPlotChartOptions = ({
|
||||
const isTextTruncated = (): boolean => {
|
||||
// For right-side legends, check if text overflows the container
|
||||
if (legendPosition === LegendPosition.RIGHT) {
|
||||
return textSpan?.scrollWidth > textSpan?.clientWidth;
|
||||
return textSpan.scrollWidth > textSpan.clientWidth;
|
||||
}
|
||||
// For bottom legends, check if text is longer than reasonable display length
|
||||
return legendText.length > 20;
|
||||
@@ -527,11 +526,9 @@ export const getUPlotChartOptions = ({
|
||||
`;
|
||||
|
||||
// Position tooltip near cursor
|
||||
const rect = (e.target as HTMLElement)?.getBoundingClientRect?.();
|
||||
if (rect) {
|
||||
tooltipElement.style.left = `${e.clientX + 10}px`;
|
||||
tooltipElement.style.top = `${rect.top - 35}px`;
|
||||
}
|
||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
||||
tooltipElement.style.left = `${e.clientX + 10}px`;
|
||||
tooltipElement.style.top = `${rect.top - 35}px`;
|
||||
|
||||
document.body.appendChild(tooltipElement);
|
||||
}, 15);
|
||||
@@ -559,7 +556,7 @@ export const getUPlotChartOptions = ({
|
||||
|
||||
// Helper function to handle stack chart logic
|
||||
const handleStackChart = (): void => {
|
||||
setHiddenGraph?.((prev) => {
|
||||
setHiddenGraph((prev) => {
|
||||
if (isUndefined(prev)) {
|
||||
return { [index]: true };
|
||||
}
|
||||
@@ -573,7 +570,7 @@ export const getUPlotChartOptions = ({
|
||||
// Marker click handler - checkbox behavior (toggle individual series)
|
||||
if (currentMarker) {
|
||||
currentMarker.addEventListener('click', (e) => {
|
||||
e.stopPropagation?.(); // Prevent event bubbling to text handler
|
||||
e.stopPropagation(); // Prevent event bubbling to text handler
|
||||
|
||||
if (stackChart) {
|
||||
handleStackChart();
|
||||
@@ -586,7 +583,7 @@ export const getUPlotChartOptions = ({
|
||||
index + 1
|
||||
];
|
||||
|
||||
saveLegendEntriesToLocalStorage?.({
|
||||
saveLegendEntriesToLocalStorage({
|
||||
options: self,
|
||||
graphVisibilityState: newGraphVisibilityStates,
|
||||
name: id || '',
|
||||
@@ -600,7 +597,7 @@ export const getUPlotChartOptions = ({
|
||||
// Text click handler - show only/show all behavior (existing behavior)
|
||||
if (textElement) {
|
||||
textElement.addEventListener('click', (e) => {
|
||||
e.stopPropagation?.(); // Prevent event bubbling
|
||||
e.stopPropagation(); // Prevent event bubbling
|
||||
|
||||
if (stackChart) {
|
||||
handleStackChart();
|
||||
@@ -622,7 +619,7 @@ export const getUPlotChartOptions = ({
|
||||
newGraphVisibilityStates.fill(false);
|
||||
newGraphVisibilityStates[index + 1] = true;
|
||||
}
|
||||
saveLegendEntriesToLocalStorage?.({
|
||||
saveLegendEntriesToLocalStorage({
|
||||
options: self,
|
||||
graphVisibilityState: newGraphVisibilityStates,
|
||||
name: id || '',
|
||||
|
||||
@@ -31,9 +31,7 @@ function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any {
|
||||
|
||||
// Fill missing timestamps with null values
|
||||
processedData.forEach((entry: { values: (number | null)[][] }) => {
|
||||
const existingTimestamps = new Set(
|
||||
(entry.values ?? []).map((value) => value[0]),
|
||||
);
|
||||
const existingTimestamps = new Set(entry.values.map((value) => value[0]));
|
||||
|
||||
const missingTimestamps = Array.from(allTimestampsSet).filter(
|
||||
(timestamp) => !existingTimestamps.has(timestamp),
|
||||
|
||||
@@ -3,7 +3,8 @@ export const dashboardSuccessResponse = {
|
||||
status: 'success',
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||
createdBy: null,
|
||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||
@@ -22,7 +23,8 @@ export const dashboardSuccessResponse = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
id: 2,
|
||||
uuid: '2',
|
||||
createdAt: '2022-11-16T13:20:47.064874419Z',
|
||||
createdBy: null,
|
||||
updatedAt: '2024-05-21T06:42:30.546630961Z',
|
||||
@@ -51,7 +53,8 @@ export const dashboardEmptyState = {
|
||||
export const getDashboardById = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: '1',
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||
createdBy: 'integration',
|
||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||
@@ -75,7 +78,8 @@ export const getDashboardById = {
|
||||
export const getNonIntegrationDashboardById = {
|
||||
status: 'success',
|
||||
data: {
|
||||
id: '1',
|
||||
id: 1,
|
||||
uuid: '1',
|
||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||
createdBy: 'thor',
|
||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||
|
||||
@@ -234,6 +234,7 @@ describe('dashboard list page', () => {
|
||||
const firstDashboardData = dashboardSuccessResponse.data[0];
|
||||
expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: firstDashboardData.uuid,
|
||||
title: firstDashboardData.data.title,
|
||||
createdAt: firstDashboardData.createdAt,
|
||||
}),
|
||||
|
||||
@@ -145,6 +145,14 @@ describe('Logs Explorer Tests', () => {
|
||||
await waitFor(() =>
|
||||
expect(queryByTestId('logs-list-virtuoso')).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
// check for data being present in the UI
|
||||
// todo[@vikrantgupta25]: skipping this for now as the formatting matching is not picking up in the CI will debug later.
|
||||
// expect(
|
||||
// queryByText(
|
||||
// `2024-02-16 02:50:22.000 | 2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}`,
|
||||
// ),
|
||||
// ).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Multiple Current Queries', async () => {
|
||||
|
||||
@@ -19,9 +19,9 @@ function DashboardPage(): JSX.Element {
|
||||
: 'Something went wrong';
|
||||
|
||||
useEffect(() => {
|
||||
const dashboardTitle = dashboardResponse.data?.data.data.title;
|
||||
const dashboardTitle = dashboardResponse.data?.data.title;
|
||||
document.title = dashboardTitle || document.title;
|
||||
}, [dashboardResponse.data?.data.data.title, isFetching]);
|
||||
}, [dashboardResponse.data?.data.title, isFetching]);
|
||||
|
||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||
return <NotFound />;
|
||||
|
||||
@@ -67,19 +67,15 @@ export default function TraceDetailsPage(): JSX.Element {
|
||||
key: 'trace-details',
|
||||
children: <TraceDetailsV2 />,
|
||||
},
|
||||
...(process.env.NODE_ENV === 'development'
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<Cone className="funnel-icon" size={16} /> Funnels
|
||||
</div>
|
||||
),
|
||||
key: 'funnels',
|
||||
children: <div />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<Cone className="funnel-icon" size={16} /> Funnels
|
||||
</div>
|
||||
),
|
||||
key: 'funnels',
|
||||
children: <div />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FilterOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Card, Tabs, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import axios from 'axios';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
@@ -18,10 +19,13 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { cloneDeep, isEmpty, set } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
@@ -36,6 +40,8 @@ import { ActionsWrapper, Container } from './styles';
|
||||
import { getTabsItems } from './utils';
|
||||
|
||||
function TracesExplorer(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
panelType,
|
||||
@@ -118,7 +124,9 @@ function TracesExplorer(): JSX.Element {
|
||||
[currentQuery, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const getUpdatedQueryForExport = useCallback((): Query => {
|
||||
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
|
||||
|
||||
const getUpdatedQueryForExport = (): Query => {
|
||||
const updatedQuery = cloneDeep(currentQuery);
|
||||
|
||||
set(
|
||||
@@ -128,7 +136,7 @@ function TracesExplorer(): JSX.Element {
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}, [currentQuery, options.selectColumns]);
|
||||
};
|
||||
|
||||
const handleExport = useCallback(
|
||||
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
||||
@@ -145,22 +153,65 @@ function TracesExplorer(): JSX.Element {
|
||||
? getUpdatedQueryForExport()
|
||||
: exportDefaultQuery;
|
||||
|
||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||
dashboard,
|
||||
query,
|
||||
widgetId,
|
||||
panelTypeParam,
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
logEvent('Traces Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
isNewDashboard,
|
||||
dashboardName: dashboard?.data?.title,
|
||||
});
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
const message =
|
||||
data.error === 'feature usage exceeded' ? (
|
||||
<span>
|
||||
Panel limit exceeded for {DataSource.TRACES} in community edition.
|
||||
Please checkout our paid plans{' '}
|
||||
<a
|
||||
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
data.error
|
||||
);
|
||||
notifications.error({
|
||||
message,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
return;
|
||||
}
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: data.payload?.uuid || '',
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
notifications.error({
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[exportDefaultQuery, panelType, safeNavigate, getUpdatedQueryForExport],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[exportDefaultQuery, notifications, panelType, updateDashboard],
|
||||
);
|
||||
|
||||
useShareBuilderUrl(defaultQuery);
|
||||
@@ -231,7 +282,11 @@ function TracesExplorer(): JSX.Element {
|
||||
|
||||
<Container className="traces-explorer-views">
|
||||
<ActionsWrapper>
|
||||
<ExportPanel query={exportDefaultQuery} onExport={handleExport} />
|
||||
<ExportPanel
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
</ActionsWrapper>
|
||||
|
||||
<Tabs
|
||||
@@ -244,6 +299,7 @@ function TracesExplorer(): JSX.Element {
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
sourcepage={DataSource.TRACES}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import './FunnelStep.styles.scss';
|
||||
|
||||
import { Button, Divider, Dropdown, Form, Space, Switch, Tooltip } from 'antd';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import { Button, Divider, Form, Switch, Tooltip } from 'antd';
|
||||
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { ChevronDown, GripVertical, HardHat, PencilLine } from 'lucide-react';
|
||||
import { LatencyPointers } from 'pages/TracesFunnelDetails/constants';
|
||||
import { GripVertical, HardHat, PencilLine } from 'lucide-react';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
@@ -37,16 +35,17 @@ function FunnelStep({
|
||||
false,
|
||||
);
|
||||
|
||||
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
|
||||
(option) => ({
|
||||
key: option.value,
|
||||
label: option.key,
|
||||
style:
|
||||
option.value === stepData.latency_pointer
|
||||
? { backgroundColor: 'var(--bg-slate-100)' }
|
||||
: {},
|
||||
}),
|
||||
);
|
||||
// temporarily hide latency pointer, as it breaks some edge cases (ref: https://signoz-team.slack.com/archives/C089MNX4Y90/p1748600682066499?thread_ts=1748599673.171759&cid=C089MNX4Y90)
|
||||
// const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
|
||||
// (option) => ({
|
||||
// key: option.value,
|
||||
// label: option.key,
|
||||
// style:
|
||||
// option.value === stepData.latency_pointer
|
||||
// ? { backgroundColor: 'var(--bg-slate-100)' }
|
||||
// : {},
|
||||
// }),
|
||||
// );
|
||||
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
@@ -176,7 +175,8 @@ function FunnelStep({
|
||||
/>
|
||||
<div className="error__label">Errors</div>
|
||||
</div>
|
||||
<div className="latency-pointer">
|
||||
{/* temporarily hide latency pointer, as it breaks some edge cases (ref: https://signoz-team.slack.com/archives/C089MNX4Y90/p1748600682066499?thread_ts=1748599673.171759&cid=C089MNX4Y90) */}
|
||||
{/* <div className="latency-pointer">
|
||||
<div className="latency-pointer__label">Latency pointer</div>
|
||||
<Dropdown
|
||||
menu={{
|
||||
@@ -197,7 +197,7 @@ function FunnelStep({
|
||||
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -170,6 +170,10 @@ export function FunnelProvider({
|
||||
handleStepUpdate(index, {
|
||||
service_name: serviceName,
|
||||
span_name: spanName,
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
});
|
||||
logEvent('Trace Funnels: span added (replaced) from trace details page', {});
|
||||
},
|
||||
@@ -191,6 +195,11 @@ export function FunnelProvider({
|
||||
funnelId,
|
||||
selectedTime,
|
||||
]);
|
||||
queryClient.refetchQueries([
|
||||
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
]);
|
||||
queryClient.refetchQueries([
|
||||
REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA,
|
||||
funnelId,
|
||||
|
||||
@@ -14,8 +14,7 @@ function TracesModulePage(): JSX.Element {
|
||||
|
||||
const routes: TabRoutes[] = [
|
||||
tracesExplorer,
|
||||
// TODO(shaheer): remove this check after everything is ready
|
||||
process.env.NODE_ENV === 'development' ? tracesFunnel(pathname) : null,
|
||||
tracesFunnel(pathname),
|
||||
tracesSaveView,
|
||||
].filter(Boolean) as TabRoutes[];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import getUserVersion from 'api/v1/version/getVersion';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import dayjs from 'dayjs';
|
||||
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
|
||||
import { useGetFeatureFlag } from 'hooks/useGetFeatureFlag';
|
||||
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
||||
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
|
||||
import useGetUser from 'hooks/user/useGetUser';
|
||||
import {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Modal } from 'antd';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import locked from 'api/v1/dashboards/id/lock';
|
||||
import getDashboard from 'api/dashboard/get';
|
||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
||||
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -16,7 +18,6 @@ import isEqual from 'lodash-es/isEqual';
|
||||
import isUndefined from 'lodash-es/isUndefined';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
@@ -35,9 +36,7 @@ import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
@@ -53,10 +52,7 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
isDashboardLocked: false,
|
||||
handleToggleDashboardSlider: () => {},
|
||||
handleDashboardLockToggle: () => {},
|
||||
dashboardResponse: {} as UseQueryResult<
|
||||
SuccessResponseV2<Dashboard>,
|
||||
APIError
|
||||
>,
|
||||
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
|
||||
selectedDashboard: {} as Dashboard,
|
||||
dashboardId: '',
|
||||
layouts: [],
|
||||
@@ -120,8 +116,6 @@ export function DashboardProvider({
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
|
||||
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
|
||||
|
||||
@@ -276,24 +270,18 @@ export function DashboardProvider({
|
||||
setIsDashboardFetching(true);
|
||||
try {
|
||||
return await getDashboard({
|
||||
id: dashboardId,
|
||||
uuid: dashboardId,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
return;
|
||||
} finally {
|
||||
setIsDashboardFetching(false);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||
onSuccess: (data) => {
|
||||
const updatedDashboardData = transformDashboardVariables(data);
|
||||
const updatedDate = dayjs(updatedDashboardData.updatedAt);
|
||||
|
||||
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||
setIsDashboardLocked(updatedDashboardData?.isLocked || false);
|
||||
|
||||
// on first render
|
||||
if (updatedTimeRef.current === null) {
|
||||
@@ -303,9 +291,7 @@ export function DashboardProvider({
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
);
|
||||
setLayouts(sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)));
|
||||
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
|
||||
}
|
||||
@@ -314,7 +300,7 @@ export function DashboardProvider({
|
||||
updatedTimeRef.current !== null &&
|
||||
updatedDate.isAfter(updatedTimeRef.current) &&
|
||||
isVisible &&
|
||||
dashboardRef.current?.id === updatedDashboardData?.id
|
||||
dashboardRef.current?.id === updatedDashboardData.id
|
||||
) {
|
||||
// show modal when state is out of sync
|
||||
const modal = onModal.confirm({
|
||||
@@ -341,20 +327,20 @@ export function DashboardProvider({
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData?.updatedAt);
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updatedAt);
|
||||
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)),
|
||||
);
|
||||
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
|
||||
setPanelMap(defaultTo(updatedDashboardData.data.panelMap, {}));
|
||||
},
|
||||
});
|
||||
|
||||
modalRef.current = modal;
|
||||
} else {
|
||||
// normal flow
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData?.updatedAt);
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData.updatedAt);
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
@@ -365,14 +351,14 @@ export function DashboardProvider({
|
||||
if (
|
||||
!isEqual(
|
||||
[omitBy(layouts, (value): boolean => isUndefined(value))[0]],
|
||||
updatedDashboardData?.data.layout,
|
||||
updatedDashboardData.data.layout,
|
||||
)
|
||||
) {
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)),
|
||||
);
|
||||
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
|
||||
setPanelMap(defaultTo(updatedDashboardData.data.panelMap, {}));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -401,25 +387,29 @@ export function DashboardProvider({
|
||||
setIsDashboardSlider(value);
|
||||
};
|
||||
|
||||
const { mutate: lockDashboard } = useMutation(locked, {
|
||||
onSuccess: (_, props) => {
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { mutate: lockDashboard } = useMutation(lockDashboardApi, {
|
||||
onSuccess: () => {
|
||||
setIsDashboardSlider(false);
|
||||
setIsDashboardLocked(props.lock);
|
||||
setIsDashboardLocked(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, {
|
||||
onSuccess: () => {
|
||||
setIsDashboardLocked(false);
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
|
||||
if (selectedDashboard) {
|
||||
try {
|
||||
await lockDashboard({
|
||||
id: selectedDashboard.id,
|
||||
lock: value,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
if (value) {
|
||||
lockDashboard(selectedDashboard);
|
||||
} else {
|
||||
unlockDashboard(selectedDashboard);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DashboardSortOrder {
|
||||
@@ -20,7 +19,7 @@ export interface IDashboardContext {
|
||||
isDashboardLocked: boolean;
|
||||
handleToggleDashboardSlider: (value: boolean) => void;
|
||||
handleDashboardLockToggle: (value: boolean) => void;
|
||||
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
||||
dashboardResponse: UseQueryResult<Dashboard, unknown>;
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
dashboardId: string;
|
||||
layouts: Layout[];
|
||||
|
||||
@@ -832,8 +832,6 @@ export function QueryBuilderProvider({
|
||||
),
|
||||
);
|
||||
}
|
||||
// Remove Hidden Filters from URL query parameters on query change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
|
||||
const generatedUrl = redirectingUrl
|
||||
? `${redirectingUrl}?${urlQuery}`
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Dashboard } from './getAll';
|
||||
import { Dashboard, DashboardData } from './getAll';
|
||||
|
||||
export type Props = {
|
||||
title: Dashboard['data']['title'];
|
||||
uploadedGrafana: boolean;
|
||||
version?: string;
|
||||
};
|
||||
export type Props =
|
||||
| {
|
||||
title: Dashboard['data']['title'];
|
||||
uploadedGrafana: boolean;
|
||||
version?: string;
|
||||
}
|
||||
| { DashboardData: DashboardData; uploadedGrafana: boolean };
|
||||
|
||||
export interface PayloadProps {
|
||||
data: Dashboard;
|
||||
status: string;
|
||||
}
|
||||
export type PayloadProps = Dashboard;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Dashboard } from './getAll';
|
||||
|
||||
export type Props = {
|
||||
id: Dashboard['id'];
|
||||
uuid: Dashboard['uuid'];
|
||||
};
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: null;
|
||||
status: 'success';
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Dashboard } from './getAll';
|
||||
|
||||
export type Props = {
|
||||
id: Dashboard['id'];
|
||||
uuid: Dashboard['uuid'];
|
||||
};
|
||||
|
||||
export interface PayloadProps {
|
||||
data: Dashboard;
|
||||
status: string;
|
||||
}
|
||||
export type PayloadProps = Dashboard;
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IField } from '../logs/fields';
|
||||
import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export type PayloadProps = Dashboard[];
|
||||
|
||||
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
|
||||
export type TVariableQueryType = typeof VariableQueryTypeArr[number];
|
||||
|
||||
@@ -48,18 +50,14 @@ export interface IDashboardVariable {
|
||||
change?: boolean;
|
||||
}
|
||||
export interface Dashboard {
|
||||
id: string;
|
||||
id: number;
|
||||
uuid: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
data: DashboardData;
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
data: Dashboard[];
|
||||
status: string;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardTemplate {
|
||||
@@ -71,7 +69,7 @@ export interface DashboardTemplate {
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
// uuid?: string;
|
||||
uuid?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
name?: string;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Dashboard } from './getAll';
|
||||
|
||||
export type Props = {
|
||||
id: Dashboard['id'];
|
||||
lock: boolean;
|
||||
};
|
||||
|
||||
export interface PayloadProps {
|
||||
data: null;
|
||||
status: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user