Compare commits

...

23 Commits

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

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

* chore(feature): some more cleanup

* chore(feature): add codeowners file

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

* chore(feature): cleanup

* chore(feature): pkg handler cleanup

* chore(feature): pkg handler cleanup

* chore(feature): address review comments

* chore(feature): address review comments

* chore(feature): address review comments

---------

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

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

---------

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

* refactor: remove comment

* fix: remove active log id on filter change

* test: update test for log explorer

* test: update test for log explorer

---------

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

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

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

* chore(dashboard): go deps

* chore(dashboard): fix lint

* chore(dashboard): implement the store

* chore(dashboard): add migration

* chore(dashboard): fix lint

* chore(dashboard): api and frontend changes

* chore(dashboard): frontend changes for new dashboards

* chore(dashboard): fix test cases

* chore(dashboard): add lock unlock APIs

* chore(dashboard): add lock unlock APIs

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

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

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

* chore(dashboard): rename migration file

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

* chore(dashboard): some testing cleanups

* chore(dashboard): fix postgres migrations

---------

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

* fix(license): fixes for license service

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

* feat: minor refactor

---------

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

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

* feat: add tracefunnel module and handler

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

* feat: add required types for tracefunnels

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

* feat: db operations, module and handler implementation

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

* feat: add db migrations

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

* chore: add utility functions

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

* test: add utility function tests

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

* test: add handler tests

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

* test: add trace funnel module tests

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

* chore: refactor handler and utils

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

* chore: add funnel validation while processing funnel steps

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

* test: add more tests to utils

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

* chore: fix package naming

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

* chore: fix naming convention

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

* chore: update normalize funnel steps

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

* chore: added some improvements

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

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

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

* chore: fix error handling

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

* feat: trace funnel state management

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

* fix: updated unit tests and mocks

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

* fix: review comments

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

* fix: minor fixes

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

* fix: update funnel migration number

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

* fix: review comments and some changes

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

* fix: update modules

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

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 07:00:49 +00:00
196 changed files with 50538 additions and 1941 deletions

2
.github/CODEOWNERS vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package httplicensing
import (
"context"
"encoding/json"
"github.com/SigNoz/signoz/ee/query-service/constants"
"time"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
@@ -12,7 +11,6 @@ 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"
@@ -88,13 +86,6 @@ 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
}
@@ -115,11 +106,6 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
return err
}
err = provider.InitFeatures(ctx, license.Features)
if err != nil {
return err
}
return nil
}
@@ -139,28 +125,24 @@ func (provider *provider) GetActive(ctx context.Context, organizationID valuer.U
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil
}
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
return err
}
if err != nil && errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals, defaulting to basic plan", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
if err != nil {
return err
}
return nil
}
return err
@@ -218,80 +200,14 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
// feature surrogate
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.store.GetFeature(ctx, key)
if err != nil {
return err
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
featureStatus, err := provider.store.GetFeature(ctx, key)
if err != nil {
return nil, err
}
return &featuretypes.GettableFeature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
storableFeatures, err := provider.store.GetAllFeatures(ctx)
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
license, err := provider.GetActive(ctx, organizationID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return licensetypes.BasicPlan, nil
}
return nil, err
}
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
for idx, gettableFeature := range storableFeatures {
gettableFeatures[idx] = &featuretypes.GettableFeature{
Name: gettableFeature.Name,
Active: gettableFeature.Active,
Usage: int64(gettableFeature.Usage),
UsageLimit: int64(gettableFeature.UsageLimit),
Route: gettableFeature.Route,
}
}
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,
})
return license.Features, nil
}

View File

@@ -5,7 +5,6 @@ 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"
)
@@ -80,81 +79,3 @@ 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
}

View File

@@ -1,7 +1,6 @@
package api
import (
"context"
"net/http"
"net/http/httputil"
"time"
@@ -86,23 +85,12 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
// invite
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
@@ -114,9 +102,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)

View File

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

View File

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

View File

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

View File

@@ -298,6 +298,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},

View File

@@ -78,7 +78,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const checkFirstTimeUser = useCallback((): boolean => {
const users = usersData?.data || [];
const remainingUsers = users.filter(
const remainingUsers = (Array.isArray(users) ? users : []).filter(
(user) => user.email !== 'admin@signoz.cloud',
);

View File

@@ -28,6 +28,7 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { Userpilot } from 'userpilot';
import { extractDomain } from 'utils/app';
@@ -171,11 +172,13 @@ function App(): JSX.Element {
user &&
!!user.email
) {
// either the active API returns error with 404 or 501 and if it returns a terminated license means it's on basic plan
const isOnBasicPlan =
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
(activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
)) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
@@ -190,6 +193,10 @@ function App(): JSX.Element {
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload {
export const saveFunnelDescription = async (
payload: UpdateFunnelDescriptionPayload,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/save`,
const response: AxiosResponse = await axios.put(
`${FUNNELS_BASE_PATH}/${payload.funnel_id}`,
payload,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
// keep this consistent with backend constants.go
// keep this consistent with backend plan.go
export enum FeatureKeys {
SSO = 'SSO',
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',
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',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
}

View File

@@ -316,7 +316,6 @@ 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(

View File

@@ -1,11 +1,12 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import createDashboard from 'api/v1/dashboards/create';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useAxiosError from 'hooks/useAxiosError';
import { useErrorModal } from 'providers/ErrorModalProvider';
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 {
@@ -33,26 +34,28 @@ function ExportPanelContainer({
refetch,
} = useGetAllDashboard();
const handleError = useAxiosError();
const { showErrorModal } = useErrorModal();
const {
mutate: createNewDashboard,
isLoading: createDashboardLoading,
} = useMutation(createDashboard, {
onSuccess: (data) => {
if (data.payload) {
onExport(data?.payload, true);
if (data.data) {
onExport(data?.data, true);
}
refetch();
},
onError: handleError,
onError: (error) => {
showErrorModal(error as APIError);
},
});
const options = useMemo(() => getSelectOptions(data || []), [data]);
const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.find(
({ uuid }) => uuid === selectedDashboardId,
const currentSelectedDashboard = data?.data?.find(
({ id }) => id === selectedDashboardId,
);
onExport(currentSelectedDashboard || null, false);
@@ -66,14 +69,18 @@ function ExportPanelContainer({
);
const handleNewDashboard = useCallback(async () => {
createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
});
}, [t, createNewDashboard]);
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]);
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;

View File

@@ -1,12 +1,10 @@
import { SelectProps } from 'antd';
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
import { Dashboard } from 'types/api/dashboard/getAll';
export const getSelectOptions = (
data: AllDashboardsData,
): SelectProps['options'] =>
data.map(({ uuid, data }) => ({
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
data.map(({ id, data }) => ({
label: data.title,
value: uuid,
value: id,
}));
export const filterOptions: SelectProps['filterOption'] = (

View File

@@ -36,7 +36,6 @@ 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);

View File

@@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});

View File

@@ -2,6 +2,7 @@ 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';
@@ -189,24 +190,26 @@ describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => {
const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider>
<Provider store={store}>
<AppProvider>
<WidgetGraphComponent
widget={mockProps.widget}
queryResponse={mockProps.queryResponse}
errorMessage={mockProps.errorMessage}
version={mockProps.version}
headerMenuList={mockProps.headerMenuList}
isWarning={mockProps.isWarning}
isFetchingResponse={mockProps.isFetchingResponse}
setRequestData={mockProps.setRequestData}
onClickHandler={mockProps.onClickHandler}
onDragSelect={mockProps.onDragSelect}
openTracesButton={mockProps.openTracesButton}
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
/>
</AppProvider>
</Provider>
<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>
</MockQueryClientProvider>,
);

View File

@@ -4,7 +4,6 @@ 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';
@@ -31,7 +30,7 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
@@ -119,29 +118,23 @@ function WidgetGraphComponent({
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
const updatedSelectedDashboard: Props = {
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
},
uuid: selectedDashboard.uuid,
id: selectedDashboard.id,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
setDeleteModal(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
@@ -166,7 +159,8 @@ function WidgetGraphComponent({
updateDashboardMutation.mutateAsync(
{
...selectedDashboard,
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
layout,
@@ -183,9 +177,9 @@ function WidgetGraphComponent({
},
{
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.',
@@ -252,7 +246,11 @@ function WidgetGraphComponent({
const graphClick = useGraphClickToShowButton({
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
isButtonEnabled: (widget?.query?.builder?.queryData ?? []).some(
isButtonEnabled: (widget?.query?.builder?.queryData &&
Array.isArray(widget.query.builder.queryData)
? widget.query.builder.queryData
: []
).some(
(q) =>
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
),

View File

@@ -160,8 +160,6 @@ 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([

View File

@@ -31,7 +31,9 @@ export const getLocalStorageGraphVisibilityState = ({
name: string;
}): GraphVisibilityLegendEntryProps => {
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
graphVisibilityStates: Array(apiResponse.length + 1).fill(true),
graphVisibilityStates: Array(
(Array.isArray(apiResponse) ? apiResponse.length : 0) + 1,
).fill(true),
legendEntry: [
{
label: 'Timestamp',

View File

@@ -6,7 +6,6 @@ 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';
@@ -14,7 +13,6 @@ 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';
@@ -36,7 +34,8 @@ import { ItemCallback, Layout } from 'react-grid-layout';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -107,7 +106,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
@@ -158,20 +156,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Dashboard Detail: Opened', {
dashboardId: data.uuid,
dashboardId: selectedDashboard?.id,
dashboardName: data.title,
numberOfPanels: data.widgets?.length,
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
});
logEventCalledRef.current = true;
}
}, [data]);
}, [data, selectedDashboard?.id]);
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
const updatedDashboard: Dashboard = {
...selectedDashboard,
const updatedDashboard: Props = {
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
panelMap: { ...currentPanelMap },
@@ -186,24 +184,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return widget;
}),
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout)
setLayouts(sortLayout(updatedDashboard.data.data.layout));
setSelectedDashboard(updatedDashboard.data);
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
}
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
@@ -286,33 +278,25 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updatedWidgets?.push(currentWidget);
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
form.setFieldValue('title', '');
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
@@ -415,12 +399,14 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
};
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
if (currentPanelMap[oldItem.i]) {
if (oldItem?.i && 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]: {
@@ -447,34 +433,26 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const updatedPanelMap = { ...currentPanelMap };
delete updatedPanelMap[currentSelectRowId];
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
panelMap: updatedPanelMap,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
const isDashboardEmpty = useMemo(

View File

@@ -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,
})),
],
},

View File

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

View File

@@ -33,7 +33,7 @@ export default function Dashboards({
useEffect(() => {
if (!dashboardsList) return;
const sortedDashboards = dashboardsList.sort((a, b) => {
const sortedDashboards = dashboardsList.data.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.uuid}`;
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
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={
dashboard.id % 2 === 0
Math.random() % 2 === 0
? '/Icons/eight-ball.svg'
: '/Icons/circus-tent.svg'
}

View File

@@ -41,7 +41,6 @@ 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,

View File

@@ -22,7 +22,7 @@ import {
} from 'antd';
import { TableProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import createDashboard from 'api/v1/dashboards/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -63,6 +63,7 @@ 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,
@@ -83,6 +84,7 @@ import {
WidgetRow,
Widgets,
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
@@ -226,7 +228,7 @@ function DashboardsList(): JSX.Element {
useEffect(() => {
const filteredDashboards = filterDashboard(
searchString,
dashboardListResponse || [],
dashboardListResponse?.data || [],
);
if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []);
@@ -256,17 +258,19 @@ function DashboardsList(): JSX.Element {
errorMessage: '',
});
const { showErrorModal } = useErrorModal();
const data: Data[] =
dashboards?.map((e) => ({
createdAt: e.createdAt,
description: e.data.description || '',
id: e.uuid,
id: e.id,
lastUpdatedTime: e.updatedAt,
name: e.data.title,
tags: e.data.tags || [],
key: e.uuid,
key: e.id,
createdBy: e.createdBy,
isLocked: !!e.isLocked || false,
isLocked: !!e.locked || false,
lastUpdatedBy: e.updatedBy,
image: e.data.image || Base64Icons[0],
variables: e.data.variables,
@@ -292,28 +296,20 @@ function DashboardsList(): JSX.Element {
version: ENTITY_VERSION_V4,
});
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',
});
}
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
} catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, safeNavigate, t]);
}, [newDashboardState, safeNavigate, showErrorModal, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
@@ -327,7 +323,7 @@ function DashboardsList(): JSX.Element {
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse || [],
dashboardListResponse?.data || [],
);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
@@ -677,7 +673,7 @@ function DashboardsList(): JSX.Element {
!isUndefined(dashboardListResponse)
) {
logEvent('Dashboard List: Page visited', {
number: dashboardListResponse?.length,
number: dashboardListResponse?.data?.length,
});
logEventCalledRef.current = true;
}

View File

@@ -14,19 +14,21 @@ import {
UploadProps,
} from 'antd';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import createDashboard from 'api/v1/dashboards/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,
@@ -74,6 +76,8 @@ function ImportJSON({
}
};
const { showErrorModal } = useErrorModal();
const onClickLoadJsonHandler = async (): Promise<void> => {
try {
setDashboardCreating(true);
@@ -81,11 +85,6 @@ 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 {
@@ -97,28 +96,19 @@ function ImportJSON({
uploadedGrafana,
});
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',
}),
});
}
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
logEvent('Dashboard List: New dashboard imported successfully', {
dashboardId: response.data?.id,
dashboardName: response.data?.data?.title,
});
setDashboardCreating(false);
} catch (error) {
showErrorModal(error as APIError);
setDashboardCreating(false);
setIsCreateDashboardError(true);
notifications.error({

View File

@@ -6,8 +6,7 @@ import { executeSearchQueries } from '../utils';
describe('executeSearchQueries', () => {
const firstDashboard: Dashboard = {
id: 11111,
uuid: uuid(),
id: uuid(),
createdAt: '',
updatedAt: '',
createdBy: '',
@@ -18,8 +17,7 @@ describe('executeSearchQueries', () => {
},
};
const secondDashboard: Dashboard = {
id: 22222,
uuid: uuid(),
id: uuid(),
createdAt: '',
updatedAt: '',
createdBy: '',
@@ -30,8 +28,7 @@ describe('executeSearchQueries', () => {
},
};
const thirdDashboard: Dashboard = {
id: 333333,
uuid: uuid(),
id: uuid(),
createdAt: '',
updatedAt: '',
createdBy: '',

View File

@@ -59,7 +59,7 @@ export function DeleteButton({
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
deleteDashboardMutation.mutateAsync(undefined, {
deleteDashboardMutation.mutate(undefined, {
onSuccess: () => {
notifications.success({
message: t('dashboard:delete_dashboard_success', {

View File

@@ -14,7 +14,7 @@ export const generateSearchData = (
dashboards.forEach((dashboard) => {
dashboardSearchData.push({
id: dashboard.uuid,
id: dashboard.id,
title: dashboard.data.title,
description: dashboard.data.description,
tags: dashboard.data.tags || [],

View File

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

View File

@@ -62,6 +62,8 @@ 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);
},

View File

@@ -28,16 +28,12 @@ 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';
@@ -98,7 +94,6 @@ 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.
@@ -141,8 +136,6 @@ 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;
@@ -195,6 +188,26 @@ 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 = {
@@ -209,7 +222,7 @@ function LogsExplorerViews({
};
return modifiedQuery;
}, [stagedQuery, listQuery]);
}, [stagedQuery, listQuery, activeLogId]);
const exportDefaultQuery = useMemo(
() =>
@@ -294,12 +307,12 @@ function LogsExplorerViews({
});
// Add filter for activeLogId if present
let updatedFilters = paginateData.filters;
let updatedFilters = params.filters;
if (activeLogId) {
updatedFilters = {
...paginateData.filters,
...params.filters,
items: [
...(paginateData.filters?.items || []),
...(params.filters?.items || []),
{
id: v4(),
key: {
@@ -396,11 +409,6 @@ 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);
@@ -424,68 +432,22 @@ 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,
});
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,
});
return;
}
const dashboardEditView = generateExportToDashboardLink({
query,
panelType: panelTypeParam,
dashboardId: data.payload?.uuid || '',
widgetId,
});
safeNavigate(dashboardEditView);
},
onError: handleAxisError,
const dashboardEditView = generateExportToDashboardLink({
query,
panelType: panelTypeParam,
dashboardId: dashboard.id,
widgetId,
});
safeNavigate(dashboardEditView);
},
[
getUpdatedQueryForExport,
exportDefaultQuery,
options.selectColumns,
safeNavigate,
notifications,
panelType,
updateDashboard,
handleAxisError,
],
[getUpdatedQueryForExport, exportDefaultQuery, safeNavigate, panelType],
);
useEffect(() => {
@@ -811,7 +773,6 @@ function LogsExplorerViews({
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading}
onExport={handleExport}
sourcepage={DataSource.LOGS}
/>

View File

@@ -1,17 +1,23 @@
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 } from './mock';
import {
logsQueryRangeSuccessNewFormatResponse,
mockQueryBuilderContextValue,
} 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 } => ({
@@ -81,6 +87,12 @@ 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({
@@ -162,4 +174,47 @@ 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);
});
});

View File

@@ -1,3 +1,13 @@
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: [],
@@ -49,3 +59,148 @@ 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,
};

View File

@@ -2,18 +2,12 @@ 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';
@@ -39,13 +33,6 @@ 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 =
@@ -86,59 +73,16 @@ function Explorer(): JSX.Element {
const widgetId = uuid();
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard,
queryToExport || exportDefaultQuery,
const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId,
PANEL_TYPES.TIME_SERIES,
options.selectColumns,
);
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,
});
}
},
});
safeNavigate(dashboardEditView);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[exportDefaultQuery, notifications, updateDashboard],
[exportDefaultQuery, safeNavigate],
);
const splitedQueries = useMemo(
@@ -201,7 +145,6 @@ function Explorer(): JSX.Element {
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
isLoading={isLoading}
sourcepage={DataSource.METRICS}
onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery}

View File

@@ -12,7 +12,6 @@ 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';
@@ -44,11 +43,8 @@ import { FullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import {
Dashboard,
DashboardData,
IDashboardVariable,
} from 'types/api/dashboard/getAll';
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
@@ -65,10 +61,9 @@ interface DashboardDescriptionProps {
export function sanitizeDashboardData(
selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> {
): DashboardData {
if (!selectedData?.variables) {
const { uuid, ...rest } = selectedData;
return rest;
return selectedData;
}
const updatedVariables = Object.entries(selectedData.variables).reduce(
@@ -80,9 +75,8 @@ export function sanitizeDashboardData(
{} as Record<string, IDashboardVariable>,
);
const { uuid, ...restData } = selectedData;
return {
...restData,
...selectedData,
variables: updatedVariables,
};
}
@@ -108,7 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const selectedData = selectedDashboard
? {
...selectedDashboard.data,
uuid: selectedDashboard.uuid,
uuid: selectedDashboard.id,
}
: ({} as DashboardData);
@@ -162,7 +156,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid,
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
@@ -178,8 +172,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
if (!selectedDashboard) {
return;
}
const updatedDashboard = {
...selectedDashboard,
const updatedDashboard: Props = {
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
title: updatedTitle,
@@ -191,13 +186,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
message: 'Dashboard renamed successfully',
});
setIsRenameDashboardOpen(false);
if (updatedDashboard.payload)
setSelectedDashboard(updatedDashboard.payload);
if (updatedDashboard.data) setSelectedDashboard(updatedDashboard.data);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
setIsRenameDashboardOpen(true);
},
});
@@ -251,8 +242,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}
}
const updatedDashboard: Dashboard = {
...selectedDashboard,
const updatedDashboard: Props = {
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
layout: [
@@ -279,28 +271,21 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
},
],
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutate(updatedDashboard, {
// eslint-disable-next-line sonarjs/no-identical-functions
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout)
setLayouts(sortLayout(updatedDashboard.data.data.layout));
setSelectedDashboard(updatedDashboard.data);
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
}
setIsPanelNameModalOpen(false);
setSectionName(DEFAULT_ROW_NAME);
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
}
@@ -445,7 +430,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DeleteButton
createdBy={selectedDashboard?.createdBy || ''}
name={selectedDashboard?.data.title || ''}
id={String(selectedDashboard?.uuid) || ''}
id={String(selectedDashboard?.id) || ''}
isLocked={isDashboardLocked}
routeToListPage
/>

View File

@@ -1,10 +1,8 @@
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';
@@ -38,14 +36,12 @@ function GeneralDashboardSettings(): JSX.Element {
const { t } = useTranslation('common');
const { notifications } = useNotifications();
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
updateDashboardMutation.mutateAsync(
updateDashboardMutation.mutate(
{
...selectedDashboard,
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
description: updatedDescription,
@@ -56,15 +52,11 @@ function GeneralDashboardSettings(): JSX.Element {
},
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
}
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
onError: () => {},
},
);
};

View File

@@ -185,7 +185,7 @@ function VariableItem({
if (details.error) {
let message = details.error;
if (details.error.includes('Syntax error:')) {
if ((details.error ?? '').toString().includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}

View File

@@ -171,7 +171,8 @@ function VariablesSetting({
updateMutation.mutateAsync(
{
...selectedDashboard,
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
variables: updatedVariablesData,
@@ -179,18 +180,13 @@ function VariablesSetting({
},
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
notifications.success({
message: t('variable_updated_successfully'),
});
}
},
onError: () => {
notifications.error({
message: t('error_while_updating_variable'),
});
},
},
);
};

View File

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

View File

@@ -229,7 +229,7 @@ function VariableItem({
if (details.error) {
let message = details.error;
if (details.error.includes('Syntax error:')) {
if ((details.error ?? '').toString().includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}

View File

@@ -117,6 +117,9 @@ 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);
});
});

View File

@@ -127,7 +127,7 @@ function QuerySection({
panelType: selectedWidget.panelTypes,
queryType: currentQuery.queryType,
widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.uuid,
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
isNewPanel,
});

View File

@@ -125,7 +125,11 @@ function WidgetGraph({
// context redirection to explorer pages
const graphClick = useGraphClickToShowButton({
graphRef,
isButtonEnabled: (selectedWidget?.query?.builder?.queryData ?? []).some(
isButtonEnabled: (selectedWidget?.query?.builder?.queryData &&
Array.isArray(selectedWidget.query.builder.queryData)
? selectedWidget.query.builder.queryData
: []
).some(
(q) =>
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
),

View File

@@ -17,7 +17,6 @@ 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';
@@ -41,10 +40,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';
@@ -141,11 +140,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
if (!logEventCalledRef.current) {
logEvent('Panel Edit: Page visited', {
panelType: selectedWidget?.panelTypes,
dashboardId: selectedDashboard?.uuid,
dashboardId: selectedDashboard?.id,
widgetId: selectedWidget?.id,
dashboardName: selectedDashboard?.data.title,
isNewPanel: !!isWidgetNotPresent,
dataSource: currentQuery.builder.queryData?.[0]?.dataSource,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
});
logEventCalledRef.current = true;
}
@@ -345,8 +344,6 @@ 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);
@@ -375,7 +372,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,
},
},
};
@@ -470,9 +467,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
updatedLayout = newLayoutItem;
}
const dashboard: Dashboard = {
...selectedDashboard,
uuid: selectedDashboard.uuid,
const dashboard: Props = {
id: selectedDashboard.id,
data: {
...selectedDashboard.data,
widgets: isNewDashboard
@@ -540,15 +537,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
};
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard);
setSelectedDashboard(updatedDashboard.data);
setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
},
onError: handleError,
});
}, [
selectedDashboard,
@@ -562,7 +558,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
currentQuery,
preWidgets,
updateDashboardMutation,
handleError,
widgets,
setSelectedDashboard,
setToScrollWidgetId,
@@ -601,12 +596,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.uuid,
dashboardId: selectedDashboard?.id,
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
@@ -614,7 +609,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;
@@ -625,7 +620,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,
@@ -637,7 +632,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return isNewTraceLogsAvailable;
}, [
currentQuery.builder.queryData,
currentQuery?.builder?.queryData,
selectedWidget?.id,
isNewTraceLogsAvailable,
]);
@@ -666,7 +661,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) => ({

View File

@@ -55,7 +55,11 @@ 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(
() =>

View File

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

View File

@@ -75,7 +75,11 @@ 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(

View File

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

View File

@@ -26,6 +26,7 @@ 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';
@@ -301,10 +302,11 @@ function SideNav(): JSX.Element {
}
const isOnBasicPlan =
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
(activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
)) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
updatedMenuItems = updatedMenuItems.filter(
@@ -353,6 +355,7 @@ function SideNav(): JSX.Element {
t,
user.role,
activeLicenseFetchError,
activeLicense?.status,
]);
return (

View File

@@ -377,6 +377,8 @@ 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);
@@ -669,9 +671,7 @@ 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]);

View File

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

View File

@@ -142,6 +142,7 @@ export const useValidateFunnelSteps = ({
interface SaveFunnelDescriptionPayload {
funnel_id: string;
description: string;
timestamp: number;
}
export const useSaveFunnelDescription = (): UseMutationResult<
@@ -149,7 +150,11 @@ export const useSaveFunnelDescription = (): UseMutationResult<
Error,
SaveFunnelDescriptionPayload
> =>
useMutation({
useMutation<
SuccessResponse<FunnelData> | ErrorResponse,
Error,
SaveFunnelDescriptionPayload
>({
mutationFn: saveFunnelDescription,
});

View File

@@ -1,15 +1,23 @@
import deleteDashboard from 'api/dashboard/delete';
import deleteDashboard from 'api/v1/dashboards/id/delete';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMutation, UseMutationResult } from 'react-query';
import { PayloadProps } from 'types/api/dashboard/delete';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
export const useDeleteDashboard = (
id: string,
): UseMutationResult<PayloadProps, unknown, void, unknown> =>
useMutation({
): UseMutationResult<SuccessResponseV2<null>, APIError, void, unknown> => {
const { showErrorModal } = useErrorModal();
return useMutation<SuccessResponseV2<null>, APIError>({
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
mutationFn: () =>
deleteDashboard({
uuid: id,
id,
}),
onError: (error: APIError) => {
showErrorModal(error);
},
});
};

View File

@@ -1,10 +1,22 @@
import { getAllDashboardList } from 'api/dashboard/getAll';
import getAll from 'api/v1/dashboards/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<Dashboard[], unknown> =>
useQuery<Dashboard[]>({
queryFn: getAllDashboardList,
export const useGetAllDashboard = (): UseQueryResult<
SuccessResponseV2<Dashboard[]>,
APIError
> => {
const { showErrorModal } = useErrorModal();
return useQuery<SuccessResponseV2<Dashboard[]>, APIError>({
queryFn: getAll,
onError: (error) => {
showErrorModal(error);
},
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
});
};

View File

@@ -1,25 +1,31 @@
import update from 'api/dashboard/update';
import update from 'api/v1/dashboards/id/update';
import dayjs from 'dayjs';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponseV2 } 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.payload) {
updatedTimeRef.current = dayjs(data.payload.updatedAt);
if (data.data) {
updatedTimeRef.current = dayjs(data.data.updatedAt);
}
},
onError: (error) => {
showErrorModal(error);
},
});
};
type UseUpdateDashboard = UseMutationResult<
SuccessResponse<Dashboard> | ErrorResponse,
unknown,
SuccessResponseV2<Dashboard>,
APIError,
Props,
unknown
>;

View File

@@ -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?.uuid,
dashboardId: selectedDashboard?.id,
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?.uuid,
dashboardId: selectedDashboard?.id,
widgetId: widget.id,
queryType: widget.query.queryType,
});

View File

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

View File

@@ -1,18 +1,29 @@
import getFeaturesFlags from 'api/features/getFeatureFlags';
import list from 'api/v1/features/list';
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';
const useGetFeatureFlag = (
export interface Props {
onSuccessHandler: (routes: FeatureFlagProps[]) => void;
isLoggedIn: boolean;
}
type UseGetFeatureFlag = UseQueryResult<
SuccessResponseV2<FeatureFlagProps[]>,
APIError
>;
export const useGetFeatureFlag = (
onSuccessHandler: (routes: FeatureFlagProps[]) => void,
isLoggedIn: boolean,
): UseQueryResult<FeatureFlagProps[], unknown> =>
useQuery<FeatureFlagProps[]>({
queryFn: getFeaturesFlags,
): UseGetFeatureFlag =>
useQuery<SuccessResponseV2<FeatureFlagProps[]>, APIError>({
queryKey: [REACT_QUERY_KEY.GET_FEATURES_FLAGS],
onSuccess: onSuccessHandler,
queryFn: () => list(),
onSuccess: (data) => {
onSuccessHandler(data.data);
},
retryOnMount: false,
enabled: !!isLoggedIn,
});
export default useGetFeatureFlag;

View File

@@ -432,18 +432,19 @@ export const getUPlotChartOptions = ({
const globalCleanupHandler = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
if (
!target.closest('.u-legend') &&
!target.classList.contains('legend-tooltip')
target &&
!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');
@@ -484,7 +485,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;
@@ -526,9 +527,11 @@ export const getUPlotChartOptions = ({
`;
// Position tooltip near cursor
const rect = (e.target as HTMLElement).getBoundingClientRect();
tooltipElement.style.left = `${e.clientX + 10}px`;
tooltipElement.style.top = `${rect.top - 35}px`;
const rect = (e.target as HTMLElement)?.getBoundingClientRect?.();
if (rect) {
tooltipElement.style.left = `${e.clientX + 10}px`;
tooltipElement.style.top = `${rect.top - 35}px`;
}
document.body.appendChild(tooltipElement);
}, 15);
@@ -556,7 +559,7 @@ export const getUPlotChartOptions = ({
// Helper function to handle stack chart logic
const handleStackChart = (): void => {
setHiddenGraph((prev) => {
setHiddenGraph?.((prev) => {
if (isUndefined(prev)) {
return { [index]: true };
}
@@ -570,7 +573,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();
@@ -583,7 +586,7 @@ export const getUPlotChartOptions = ({
index + 1
];
saveLegendEntriesToLocalStorage({
saveLegendEntriesToLocalStorage?.({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',
@@ -597,7 +600,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();
@@ -619,7 +622,7 @@ export const getUPlotChartOptions = ({
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[index + 1] = true;
}
saveLegendEntriesToLocalStorage({
saveLegendEntriesToLocalStorage?.({
options: self,
graphVisibilityState: newGraphVisibilityStates,
name: id || '',

View File

@@ -31,7 +31,9 @@ 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),

View File

@@ -3,8 +3,7 @@ export const dashboardSuccessResponse = {
status: 'success',
data: [
{
id: 1,
uuid: '1',
id: '1',
createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: null,
updatedAt: '2024-05-21T06:41:30.546630961Z',
@@ -23,8 +22,7 @@ export const dashboardSuccessResponse = {
},
},
{
id: 2,
uuid: '2',
id: '2',
createdAt: '2022-11-16T13:20:47.064874419Z',
createdBy: null,
updatedAt: '2024-05-21T06:42:30.546630961Z',
@@ -53,8 +51,7 @@ export const dashboardEmptyState = {
export const getDashboardById = {
status: 'success',
data: {
id: 1,
uuid: '1',
id: '1',
createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: 'integration',
updatedAt: '2024-05-21T06:41:30.546630961Z',
@@ -78,8 +75,7 @@ export const getDashboardById = {
export const getNonIntegrationDashboardById = {
status: 'success',
data: {
id: 1,
uuid: '1',
id: '1',
createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: 'thor',
updatedAt: '2024-05-21T06:41:30.546630961Z',

View File

@@ -234,7 +234,6 @@ 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,
}),

View File

@@ -145,14 +145,6 @@ 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 () => {

View File

@@ -19,9 +19,9 @@ function DashboardPage(): JSX.Element {
: 'Something went wrong';
useEffect(() => {
const dashboardTitle = dashboardResponse.data?.data.title;
const dashboardTitle = dashboardResponse.data?.data.data.title;
document.title = dashboardTitle || document.title;
}, [dashboardResponse.data?.data.title, isFetching]);
}, [dashboardResponse.data?.data.data.title, isFetching]);
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
return <NotFound />;

View File

@@ -4,7 +4,6 @@ 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';
@@ -19,13 +18,10 @@ 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';
@@ -40,8 +36,6 @@ import { ActionsWrapper, Container } from './styles';
import { getTabsItems } from './utils';
function TracesExplorer(): JSX.Element {
const { notifications } = useNotifications();
const {
currentQuery,
panelType,
@@ -124,9 +118,7 @@ function TracesExplorer(): JSX.Element {
[currentQuery, updateAllQueriesOperators],
);
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
const getUpdatedQueryForExport = (): Query => {
const getUpdatedQueryForExport = useCallback((): Query => {
const updatedQuery = cloneDeep(currentQuery);
set(
@@ -136,7 +128,7 @@ function TracesExplorer(): JSX.Element {
);
return updatedQuery;
};
}, [currentQuery, options.selectColumns]);
const handleExport = useCallback(
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
@@ -153,65 +145,22 @@ 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,
});
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,
});
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,
});
}
},
const dashboardEditView = generateExportToDashboardLink({
query,
panelType: panelTypeParam,
dashboardId: dashboard.id,
widgetId,
});
safeNavigate(dashboardEditView);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[exportDefaultQuery, notifications, panelType, updateDashboard],
[exportDefaultQuery, panelType, safeNavigate, getUpdatedQueryForExport],
);
useShareBuilderUrl(defaultQuery);
@@ -282,11 +231,7 @@ function TracesExplorer(): JSX.Element {
<Container className="traces-explorer-views">
<ActionsWrapper>
<ExportPanel
query={exportDefaultQuery}
isLoading={isLoading}
onExport={handleExport}
/>
<ExportPanel query={exportDefaultQuery} onExport={handleExport} />
</ActionsWrapper>
<Tabs
@@ -299,7 +244,6 @@ function TracesExplorer(): JSX.Element {
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
isLoading={isLoading}
sourcepage={DataSource.TRACES}
onExport={handleExport}
/>

View File

@@ -41,6 +41,7 @@ function AddFunnelDescriptionModal({
{
funnel_id: funnelId,
description,
timestamp: Date.now(),
},
{
onSuccess: () => {

View File

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

View File

@@ -1,14 +1,12 @@
/* eslint-disable no-nested-ternary */
import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get';
import lockDashboardApi from 'api/dashboard/lockDashboard';
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
import getDashboard from 'api/v1/dashboards/id/get';
import locked from 'api/v1/dashboards/id/lock';
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';
@@ -18,6 +16,7 @@ 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,
@@ -36,7 +35,9 @@ 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';
@@ -52,7 +53,10 @@ const DashboardContext = createContext<IDashboardContext>({
isDashboardLocked: false,
handleToggleDashboardSlider: () => {},
handleDashboardLockToggle: () => {},
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
dashboardResponse: {} as UseQueryResult<
SuccessResponseV2<Dashboard>,
APIError
>,
selectedDashboard: {} as Dashboard,
dashboardId: '',
layouts: [],
@@ -116,6 +120,8 @@ 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'];
@@ -270,18 +276,24 @@ export function DashboardProvider({
setIsDashboardFetching(true);
try {
return await getDashboard({
uuid: dashboardId,
id: dashboardId,
});
} catch (error) {
showErrorModal(error as APIError);
return;
} finally {
setIsDashboardFetching(false);
}
},
refetchOnWindowFocus: false,
onSuccess: (data) => {
const updatedDashboardData = transformDashboardVariables(data);
const updatedDate = dayjs(updatedDashboardData.updatedAt);
onError: (error) => {
showErrorModal(error as APIError);
},
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
const updatedDashboardData = transformDashboardVariables(data?.data);
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
setIsDashboardLocked(updatedDashboardData?.isLocked || false);
setIsDashboardLocked(updatedDashboardData?.locked || false);
// on first render
if (updatedTimeRef.current === null) {
@@ -291,7 +303,9 @@ export function DashboardProvider({
dashboardRef.current = updatedDashboardData;
setLayouts(sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)));
setLayouts(
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
);
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
}
@@ -300,7 +314,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({
@@ -327,20 +341,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;
@@ -351,14 +365,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, {}));
}
}
},
@@ -387,29 +401,25 @@ export function DashboardProvider({
setIsDashboardSlider(value);
};
const handleError = useAxiosError();
const { mutate: lockDashboard } = useMutation(lockDashboardApi, {
onSuccess: () => {
const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
setIsDashboardSlider(false);
setIsDashboardLocked(true);
setIsDashboardLocked(props.lock);
},
onError: handleError,
});
const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, {
onSuccess: () => {
setIsDashboardLocked(false);
onError: (error) => {
showErrorModal(error as APIError);
},
onError: handleError,
});
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
if (selectedDashboard) {
if (value) {
lockDashboard(selectedDashboard);
} else {
unlockDashboard(selectedDashboard);
try {
await lockDashboard({
id: selectedDashboard.id,
lock: value,
});
} catch (error) {
showErrorModal(error as APIError);
}
}
};

View File

@@ -1,6 +1,7 @@
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 {
@@ -19,7 +20,7 @@ export interface IDashboardContext {
isDashboardLocked: boolean;
handleToggleDashboardSlider: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<Dashboard, unknown>;
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined;
dashboardId: string;
layouts: Layout[];

View File

@@ -832,6 +832,8 @@ export function QueryBuilderProvider({
),
);
}
// Remove Hidden Filters from URL query parameters on query change
urlQuery.delete(QueryParams.activeLogId);
const generatedUrl = redirectingUrl
? `${redirectingUrl}?${urlQuery}`

View File

@@ -1,11 +1,12 @@
import { Dashboard, DashboardData } from './getAll';
import { Dashboard } from './getAll';
export type Props =
| {
title: Dashboard['data']['title'];
uploadedGrafana: boolean;
version?: string;
}
| { DashboardData: DashboardData; uploadedGrafana: boolean };
export type Props = {
title: Dashboard['data']['title'];
uploadedGrafana: boolean;
version?: string;
};
export type PayloadProps = Dashboard;
export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@@ -1,9 +1,10 @@
import { Dashboard } from './getAll';
export type Props = {
uuid: Dashboard['uuid'];
id: Dashboard['id'];
};
export interface PayloadProps {
status: 'success';
status: string;
data: null;
}

View File

@@ -1,7 +1,10 @@
import { Dashboard } from './getAll';
export type Props = {
uuid: Dashboard['uuid'];
id: Dashboard['id'];
};
export type PayloadProps = Dashboard;
export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@@ -9,8 +9,6 @@ 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];
@@ -50,14 +48,18 @@ export interface IDashboardVariable {
change?: boolean;
}
export interface Dashboard {
id: number;
uuid: string;
id: string;
createdAt: string;
updatedAt: string;
createdBy: string;
updatedBy: string;
data: DashboardData;
isLocked?: boolean;
locked?: boolean;
}
export interface PayloadProps {
data: Dashboard[];
status: string;
}
export interface DashboardTemplate {
@@ -69,7 +71,7 @@ export interface DashboardTemplate {
}
export interface DashboardData {
uuid?: string;
// uuid?: string;
description?: string;
tags?: string[];
name?: string;

View File

@@ -0,0 +1,11 @@
import { Dashboard } from './getAll';
export type Props = {
id: Dashboard['id'];
lock: boolean;
};
export interface PayloadProps {
data: null;
status: string;
}

View File

@@ -1,8 +1,11 @@
import { Dashboard, DashboardData } from './getAll';
export type Props = {
uuid: Dashboard['uuid'];
id: Dashboard['id'];
data: DashboardData;
};
export type PayloadProps = Dashboard;
export interface PayloadProps {
data: Dashboard;
status: string;
}

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