mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-30 13:51:59 +08:00
Merge branch 'main' into stmt-builder-metrics
This commit is contained in:
commit
ded03c04d9
1
.gitignore
vendored
1
.gitignore
vendored
@ -66,6 +66,7 @@ e2e/.auth
|
|||||||
# go
|
# go
|
||||||
vendor/
|
vendor/
|
||||||
**/main/**
|
**/main/**
|
||||||
|
__debug_bin**
|
||||||
|
|
||||||
# git-town
|
# git-town
|
||||||
.git-branches.toml
|
.git-branches.toml
|
||||||
|
@ -207,3 +207,11 @@ emailing:
|
|||||||
key_file_path:
|
key_file_path:
|
||||||
# The path to the certificate file.
|
# The path to the certificate file.
|
||||||
cert_file_path:
|
cert_file_path:
|
||||||
|
|
||||||
|
##################### Sharder (experimental) #####################
|
||||||
|
sharder:
|
||||||
|
# Specifies the sharder provider to use.
|
||||||
|
provider: noop
|
||||||
|
single:
|
||||||
|
# The org id to which this instance belongs to.
|
||||||
|
org_id: org_id
|
||||||
|
@ -3,13 +3,15 @@ package httplicensing
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
@ -19,23 +21,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type provider struct {
|
type provider struct {
|
||||||
store licensetypes.Store
|
store licensetypes.Store
|
||||||
zeus zeus.Zeus
|
zeus zeus.Zeus
|
||||||
config licensing.Config
|
config licensing.Config
|
||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
stopChan chan struct{}
|
orgGetter organization.Getter
|
||||||
|
stopChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||||
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
|
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
|
||||||
return New(ctx, providerSettings, config, store, zeus)
|
return New(ctx, providerSettings, config, store, zeus, orgGetter)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus) (licensing.Licensing, error) {
|
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
|
||||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
|
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
|
||||||
licensestore := sqllicensingstore.New(sqlstore)
|
licensestore := sqllicensingstore.New(sqlstore)
|
||||||
return &provider{store: licensestore, zeus: zeus, config: config, settings: settings, stopChan: make(chan struct{})}, nil
|
return &provider{
|
||||||
|
store: licensestore,
|
||||||
|
zeus: zeus,
|
||||||
|
config: config,
|
||||||
|
settings: settings,
|
||||||
|
orgGetter: orgGetter,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Start(ctx context.Context) error {
|
func (provider *provider) Start(ctx context.Context) error {
|
||||||
@ -67,13 +77,13 @@ func (provider *provider) Stop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Validate(ctx context.Context) error {
|
func (provider *provider) Validate(ctx context.Context) error {
|
||||||
organizations, err := provider.store.ListOrganizations(ctx)
|
organizations, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, organizationID := range organizations {
|
for _, organization := range organizations {
|
||||||
err := provider.Refresh(ctx, organizationID)
|
err := provider.Refresh(ctx, organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -168,6 +178,11 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = provider.InitFeatures(ctx, activeLicense.Features)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
@ -82,31 +81,6 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *store) ListOrganizations(ctx context.Context) ([]valuer.UUID, error) {
|
|
||||||
orgIDStrs := make([]string, 0)
|
|
||||||
err := store.sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewSelect().
|
|
||||||
Model(new(types.Organization)).
|
|
||||||
Column("id").
|
|
||||||
Scan(ctx, &orgIDStrs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
orgIDs := make([]valuer.UUID, len(orgIDStrs))
|
|
||||||
for idx, orgIDStr := range orgIDStrs {
|
|
||||||
orgID, err := valuer.NewUUID(orgIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
orgIDs[idx] = orgID
|
|
||||||
}
|
|
||||||
|
|
||||||
return orgIDs, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
|
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
|
||||||
_, err := store.
|
_, err := store.
|
||||||
sqlstore.
|
sqlstore.
|
||||||
|
@ -96,13 +96,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
|||||||
// note: add ee override methods first
|
// note: add ee override methods first
|
||||||
|
|
||||||
// routes available only in ee version
|
// routes available only in ee version
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
// invite
|
|
||||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
|
|
||||||
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
// paid plans specific routes
|
// paid plans specific routes
|
||||||
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
||||||
@ -114,9 +108,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/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/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
|
// v3
|
||||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
|
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)
|
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
|
||||||
|
@ -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")
|
|
||||||
}
|
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
@ -113,6 +114,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
serverOptions.SigNoz.SQLStore,
|
serverOptions.SigNoz.SQLStore,
|
||||||
serverOptions.SigNoz.TelemetryStore,
|
serverOptions.SigNoz.TelemetryStore,
|
||||||
serverOptions.SigNoz.Prometheus,
|
serverOptions.SigNoz.Prometheus,
|
||||||
|
serverOptions.SigNoz.Modules.OrgGetter,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -157,7 +159,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the usagemanager
|
// start the usagemanager
|
||||||
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.Organization)
|
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.OrgGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -225,7 +227,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
&opAmpModel.AllAgents, agentConfMgr,
|
&opAmpModel.AllAgents, agentConfMgr,
|
||||||
)
|
)
|
||||||
|
|
||||||
orgs, err := apiHandler.Signoz.Modules.Organization.GetAll(context.Background())
|
orgs, err := apiHandler.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -240,11 +242,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
|
func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, error) {
|
||||||
|
|
||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
|
||||||
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
||||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
s.serverOptions.Config.APIServer.Timeout.Default,
|
||||||
@ -275,8 +276,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.serverOptions.SigNoz.Sharder, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.SigNoz.Sharder).Wrap)
|
||||||
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
||||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
s.serverOptions.Config.APIServer.Timeout.Default,
|
||||||
@ -297,6 +298,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||||
apiHandler.MetricExplorerRoutes(r, am)
|
apiHandler.MetricExplorerRoutes(r, am)
|
||||||
|
apiHandler.RegisterTraceFunnelsRoutes(r, am)
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
@ -450,6 +452,7 @@ func makeRulesManager(
|
|||||||
sqlstore sqlstore.SQLStore,
|
sqlstore sqlstore.SQLStore,
|
||||||
telemetryStore telemetrystore.TelemetryStore,
|
telemetryStore telemetrystore.TelemetryStore,
|
||||||
prometheus prometheus.Prometheus,
|
prometheus prometheus.Prometheus,
|
||||||
|
orgGetter organization.Getter,
|
||||||
) (*baserules.Manager, error) {
|
) (*baserules.Manager, error) {
|
||||||
// create manager opts
|
// create manager opts
|
||||||
managerOpts := &baserules.ManagerOptions{
|
managerOpts := &baserules.ManagerOptions{
|
||||||
@ -465,6 +468,7 @@ func makeRulesManager(
|
|||||||
PrepareTestRuleFunc: rules.TestNotification,
|
PrepareTestRuleFunc: rules.TestNotification,
|
||||||
Alertmanager: alertmanager,
|
Alertmanager: alertmanager,
|
||||||
SQLStore: sqlstore,
|
SQLStore: sqlstore,
|
||||||
|
OrgGetter: orgGetter,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create Manager
|
// create Manager
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
|
pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
@ -133,8 +134,8 @@ func main() {
|
|||||||
zeus.Config(),
|
zeus.Config(),
|
||||||
httpzeus.NewProviderFactory(),
|
httpzeus.NewProviderFactory(),
|
||||||
licensing.Config(24*time.Hour, 3),
|
licensing.Config(24*time.Hour, 3),
|
||||||
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
||||||
return httplicensing.NewProviderFactory(sqlstore, zeus)
|
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
|
||||||
},
|
},
|
||||||
signoz.NewEmailingProviderFactories(),
|
signoz.NewEmailingProviderFactories(),
|
||||||
signoz.NewCacheProviderFactories(),
|
signoz.NewCacheProviderFactories(),
|
||||||
|
@ -41,16 +41,16 @@ type Manager struct {
|
|||||||
|
|
||||||
zeus zeus.Zeus
|
zeus zeus.Zeus
|
||||||
|
|
||||||
organizationModule organization.Module
|
orgGetter organization.Getter
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, organizationModule organization.Module) (*Manager, error) {
|
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
clickhouseConn: clickhouseConn,
|
clickhouseConn: clickhouseConn,
|
||||||
licenseService: licenseService,
|
licenseService: licenseService,
|
||||||
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
||||||
zeus: zeus,
|
zeus: zeus,
|
||||||
organizationModule: organizationModule,
|
orgGetter: orgGetter,
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@ -74,8 +74,7 @@ func (lm *Manager) Start(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (lm *Manager) UploadUsage(ctx context.Context) {
|
func (lm *Manager) UploadUsage(ctx context.Context) {
|
||||||
|
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
|
||||||
organizations, err := lm.organizationModule.GetAll(context.Background())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to get organizations", zap.Error(err))
|
zap.L().Error("failed to get organizations", zap.Error(err))
|
||||||
return
|
return
|
||||||
|
@ -28,6 +28,7 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
|||||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
import { CompatRouter } from 'react-router-dom-v5-compat';
|
import { CompatRouter } from 'react-router-dom-v5-compat';
|
||||||
|
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||||
import { Userpilot } from 'userpilot';
|
import { Userpilot } from 'userpilot';
|
||||||
import { extractDomain } from 'utils/app';
|
import { extractDomain } from 'utils/app';
|
||||||
|
|
||||||
@ -171,11 +172,13 @@ function App(): JSX.Element {
|
|||||||
user &&
|
user &&
|
||||||
!!user.email
|
!!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 =
|
const isOnBasicPlan =
|
||||||
activeLicenseFetchError &&
|
(activeLicenseFetchError &&
|
||||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||||
activeLicenseFetchError?.getHttpStatusCode(),
|
activeLicenseFetchError?.getHttpStatusCode(),
|
||||||
);
|
)) ||
|
||||||
|
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
|
||||||
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
||||||
|
|
||||||
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
||||||
@ -190,6 +193,10 @@ function App(): JSX.Element {
|
|||||||
updatedRoutes = updatedRoutes.filter(
|
updatedRoutes = updatedRoutes.filter(
|
||||||
(route) => route?.path !== ROUTES.BILLING,
|
(route) => route?.path !== ROUTES.BILLING,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isEnterpriseSelfHostedUser) {
|
||||||
|
updatedRoutes.push(LIST_LICENSES);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// always add support route for cloud users
|
// always add support route for cloud users
|
||||||
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
||||||
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
23
frontend/src/api/v1/dashboards/create.ts
Normal file
23
frontend/src/api/v1/dashboards/create.ts
Normal 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;
|
19
frontend/src/api/v1/dashboards/getAll.ts
Normal file
19
frontend/src/api/v1/dashboards/getAll.ts
Normal 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;
|
21
frontend/src/api/v1/dashboards/id/delete.ts
Normal file
21
frontend/src/api/v1/dashboards/id/delete.ts
Normal 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;
|
20
frontend/src/api/v1/dashboards/id/get.ts
Normal file
20
frontend/src/api/v1/dashboards/id/get.ts
Normal 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;
|
23
frontend/src/api/v1/dashboards/id/lock.ts
Normal file
23
frontend/src/api/v1/dashboards/id/lock.ts
Normal 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;
|
23
frontend/src/api/v1/dashboards/id/update.ts
Normal file
23
frontend/src/api/v1/dashboards/id/update.ts
Normal 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;
|
@ -16,6 +16,7 @@ import JSONView from 'container/LogDetailedView/JsonView';
|
|||||||
import Overview from 'container/LogDetailedView/Overview';
|
import Overview from 'container/LogDetailedView/Overview';
|
||||||
import {
|
import {
|
||||||
aggregateAttributesResourcesToString,
|
aggregateAttributesResourcesToString,
|
||||||
|
escapeHtml,
|
||||||
removeEscapeCharacters,
|
removeEscapeCharacters,
|
||||||
unescapeString,
|
unescapeString,
|
||||||
} from 'container/LogDetailedView/utils';
|
} from 'container/LogDetailedView/utils';
|
||||||
@ -118,7 +119,7 @@ function LogDetail({
|
|||||||
const htmlBody = useMemo(
|
const htmlBody = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(unescapeString(log?.body || ''), {
|
dompurify.sanitize(unescapeString(escapeHtml(log?.body || '')), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -7,7 +7,7 @@ import cx from 'classnames';
|
|||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
@ -58,7 +58,7 @@ function LogGeneralField({
|
|||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(unescapeString(fieldValue), {
|
dompurify.sanitize(unescapeString(escapeHtml(fieldValue)), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,7 @@ import { DrawerProps } from 'antd';
|
|||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { escapeHtml, unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
@ -177,7 +177,7 @@ function RawLogView({
|
|||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(unescapeString(text), {
|
dompurify.sanitize(unescapeString(escapeHtml(text)), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Button, Typography } from 'antd';
|
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 { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
import { ExportPanelProps } from '.';
|
import { ExportPanelProps } from '.';
|
||||||
import {
|
import {
|
||||||
@ -33,26 +34,28 @@ function ExportPanelContainer({
|
|||||||
refetch,
|
refetch,
|
||||||
} = useGetAllDashboard();
|
} = useGetAllDashboard();
|
||||||
|
|
||||||
const handleError = useAxiosError();
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: createNewDashboard,
|
mutate: createNewDashboard,
|
||||||
isLoading: createDashboardLoading,
|
isLoading: createDashboardLoading,
|
||||||
} = useMutation(createDashboard, {
|
} = useMutation(createDashboard, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.payload) {
|
if (data.data) {
|
||||||
onExport(data?.payload, true);
|
onExport(data?.data, true);
|
||||||
}
|
}
|
||||||
refetch();
|
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 handleExportClick = useCallback((): void => {
|
||||||
const currentSelectedDashboard = data?.find(
|
const currentSelectedDashboard = data?.data?.find(
|
||||||
({ uuid }) => uuid === selectedDashboardId,
|
({ id }) => id === selectedDashboardId,
|
||||||
);
|
);
|
||||||
|
|
||||||
onExport(currentSelectedDashboard || null, false);
|
onExport(currentSelectedDashboard || null, false);
|
||||||
@ -66,14 +69,18 @@ function ExportPanelContainer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleNewDashboard = useCallback(async () => {
|
const handleNewDashboard = useCallback(async () => {
|
||||||
createNewDashboard({
|
try {
|
||||||
title: t('new_dashboard_title', {
|
await createNewDashboard({
|
||||||
ns: 'dashboard',
|
title: t('new_dashboard_title', {
|
||||||
}),
|
ns: 'dashboard',
|
||||||
uploadedGrafana: false,
|
}),
|
||||||
version: ENTITY_VERSION_V4,
|
uploadedGrafana: false,
|
||||||
});
|
version: ENTITY_VERSION_V4,
|
||||||
}, [t, createNewDashboard]);
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
|
}
|
||||||
|
}, [createNewDashboard, t, showErrorModal]);
|
||||||
|
|
||||||
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
|
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
|
||||||
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { SelectProps } from 'antd';
|
import { SelectProps } from 'antd';
|
||||||
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
export const getSelectOptions = (
|
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
|
||||||
data: AllDashboardsData,
|
data.map(({ id, data }) => ({
|
||||||
): SelectProps['options'] =>
|
|
||||||
data.map(({ uuid, data }) => ({
|
|
||||||
label: data.title,
|
label: data.title,
|
||||||
value: uuid,
|
value: id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const filterOptions: SelectProps['filterOption'] = (
|
export const filterOptions: SelectProps['filterOption'] = (
|
||||||
|
@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
setSelectedRowWidgetId(null);
|
setSelectedRowWidgetId(null);
|
||||||
handleToggleDashboardSlider(true);
|
handleToggleDashboardSlider(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { AppProvider } from 'providers/App/App';
|
import { AppProvider } from 'providers/App/App';
|
||||||
|
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
@ -189,24 +190,26 @@ describe('WidgetGraphComponent', () => {
|
|||||||
it('should show correct menu items when hovering over more options while loading', async () => {
|
it('should show correct menu items when hovering over more options while loading', async () => {
|
||||||
const { getByTestId, findByRole, getByText, container } = render(
|
const { getByTestId, findByRole, getByText, container } = render(
|
||||||
<MockQueryClientProvider>
|
<MockQueryClientProvider>
|
||||||
<Provider store={store}>
|
<ErrorModalProvider>
|
||||||
<AppProvider>
|
<Provider store={store}>
|
||||||
<WidgetGraphComponent
|
<AppProvider>
|
||||||
widget={mockProps.widget}
|
<WidgetGraphComponent
|
||||||
queryResponse={mockProps.queryResponse}
|
widget={mockProps.widget}
|
||||||
errorMessage={mockProps.errorMessage}
|
queryResponse={mockProps.queryResponse}
|
||||||
version={mockProps.version}
|
errorMessage={mockProps.errorMessage}
|
||||||
headerMenuList={mockProps.headerMenuList}
|
version={mockProps.version}
|
||||||
isWarning={mockProps.isWarning}
|
headerMenuList={mockProps.headerMenuList}
|
||||||
isFetchingResponse={mockProps.isFetchingResponse}
|
isWarning={mockProps.isWarning}
|
||||||
setRequestData={mockProps.setRequestData}
|
isFetchingResponse={mockProps.isFetchingResponse}
|
||||||
onClickHandler={mockProps.onClickHandler}
|
setRequestData={mockProps.setRequestData}
|
||||||
onDragSelect={mockProps.onDragSelect}
|
onClickHandler={mockProps.onClickHandler}
|
||||||
openTracesButton={mockProps.openTracesButton}
|
onDragSelect={mockProps.onDragSelect}
|
||||||
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
|
openTracesButton={mockProps.openTracesButton}
|
||||||
/>
|
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
|
||||||
</AppProvider>
|
/>
|
||||||
</Provider>
|
</AppProvider>
|
||||||
|
</Provider>
|
||||||
|
</ErrorModalProvider>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import { Skeleton, Tooltip, Typography } from 'antd';
|
|||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
|
||||||
@ -31,7 +30,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
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 { DataSource } from 'types/common/queryBuilder';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -119,29 +118,23 @@ function WidgetGraphComponent({
|
|||||||
const updatedLayout =
|
const updatedLayout =
|
||||||
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
|
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
|
||||||
|
|
||||||
const updatedSelectedDashboard: Dashboard = {
|
const updatedSelectedDashboard: Props = {
|
||||||
...selectedDashboard,
|
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
widgets: updatedWidgets,
|
widgets: updatedWidgets,
|
||||||
layout: updatedLayout,
|
layout: updatedLayout,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
id: selectedDashboard.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
if (setSelectedDashboard && updatedDashboard.data) {
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
}
|
}
|
||||||
setDeleteModal(false);
|
setDeleteModal(false);
|
||||||
},
|
},
|
||||||
onError: () => {
|
|
||||||
notifications.error({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,7 +159,8 @@ function WidgetGraphComponent({
|
|||||||
|
|
||||||
updateDashboardMutation.mutateAsync(
|
updateDashboardMutation.mutateAsync(
|
||||||
{
|
{
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
layout,
|
layout,
|
||||||
@ -183,9 +177,9 @@ function WidgetGraphComponent({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
if (setSelectedDashboard && updatedDashboard.data) {
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
}
|
}
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||||
|
@ -6,7 +6,6 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
|
|||||||
import { useForm } from 'antd/es/form/Form';
|
import { useForm } from 'antd/es/form/Form';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { themeColors } from 'constants/theme';
|
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 { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { defaultTo, isUndefined } from 'lodash-es';
|
import { defaultTo, isUndefined } from 'lodash-es';
|
||||||
@ -36,7 +34,8 @@ import { ItemCallback, Layout } from 'react-grid-layout';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
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 { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
@ -107,7 +106,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const updateDashboardMutation = useUpdateDashboard();
|
const updateDashboardMutation = useUpdateDashboard();
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
||||||
@ -158,20 +156,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||||
logEvent('Dashboard Detail: Opened', {
|
logEvent('Dashboard Detail: Opened', {
|
||||||
dashboardId: data.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: data.title,
|
dashboardName: data.title,
|
||||||
numberOfPanels: data.widgets?.length,
|
numberOfPanels: data.widgets?.length,
|
||||||
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, selectedDashboard?.id]);
|
||||||
|
|
||||||
const onSaveHandler = (): void => {
|
const onSaveHandler = (): void => {
|
||||||
if (!selectedDashboard) return;
|
if (!selectedDashboard) return;
|
||||||
|
|
||||||
const updatedDashboard: Dashboard = {
|
const updatedDashboard: Props = {
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
panelMap: { ...currentPanelMap },
|
panelMap: { ...currentPanelMap },
|
||||||
@ -186,24 +184,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
return widget;
|
return widget;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateDashboardMutation.mutate(updatedDashboard, {
|
updateDashboardMutation.mutate(updatedDashboard, {
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
setSelectedRowWidgetId(null);
|
setSelectedRowWidgetId(null);
|
||||||
if (updatedDashboard.payload) {
|
if (updatedDashboard.data) {
|
||||||
if (updatedDashboard.payload.data.layout)
|
if (updatedDashboard.data.data.layout)
|
||||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
setLayouts(sortLayout(updatedDashboard.data.data.layout));
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
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);
|
updatedWidgets?.push(currentWidget);
|
||||||
|
|
||||||
const updatedSelectedDashboard: Dashboard = {
|
const updatedSelectedDashboard: Props = {
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
widgets: updatedWidgets,
|
widgets: updatedWidgets,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
if (setSelectedDashboard && updatedDashboard.data) {
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
}
|
}
|
||||||
if (setPanelMap)
|
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
|
||||||
form.setFieldValue('title', '');
|
form.setFieldValue('title', '');
|
||||||
setIsSettingsModalOpen(false);
|
setIsSettingsModalOpen(false);
|
||||||
setCurrentSelectRowId(null);
|
setCurrentSelectRowId(null);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
||||||
onError: () => {
|
|
||||||
notifications.error({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -447,34 +431,26 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
const updatedPanelMap = { ...currentPanelMap };
|
const updatedPanelMap = { ...currentPanelMap };
|
||||||
delete updatedPanelMap[currentSelectRowId];
|
delete updatedPanelMap[currentSelectRowId];
|
||||||
|
|
||||||
const updatedSelectedDashboard: Dashboard = {
|
const updatedSelectedDashboard: Props = {
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
widgets: updatedWidgets,
|
widgets: updatedWidgets,
|
||||||
layout: updatedLayout,
|
layout: updatedLayout,
|
||||||
panelMap: updatedPanelMap,
|
panelMap: updatedPanelMap,
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
if (setSelectedDashboard && updatedDashboard.data) {
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
}
|
}
|
||||||
if (setPanelMap)
|
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setCurrentSelectRowId(null);
|
setCurrentSelectRowId(null);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
||||||
onError: () => {
|
|
||||||
notifications.error({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const isDashboardEmpty = useMemo(
|
const isDashboardEmpty = useMemo(
|
||||||
|
@ -33,7 +33,7 @@ export default function Dashboards({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dashboardsList) return;
|
if (!dashboardsList) return;
|
||||||
|
|
||||||
const sortedDashboards = dashboardsList.sort((a, b) => {
|
const sortedDashboards = dashboardsList.data.sort((a, b) => {
|
||||||
const aUpdateAt = new Date(a.updatedAt).getTime();
|
const aUpdateAt = new Date(a.updatedAt).getTime();
|
||||||
const bUpdateAt = new Date(b.updatedAt).getTime();
|
const bUpdateAt = new Date(b.updatedAt).getTime();
|
||||||
return bUpdateAt - aUpdateAt;
|
return bUpdateAt - aUpdateAt;
|
||||||
@ -103,7 +103,7 @@ export default function Dashboards({
|
|||||||
<div className="home-dashboards-list-container home-data-item-container">
|
<div className="home-dashboards-list-container home-data-item-container">
|
||||||
<div className="dashboards-list">
|
<div className="dashboards-list">
|
||||||
{sortedDashboards.slice(0, 5).map((dashboard) => {
|
{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 => {
|
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -134,7 +134,7 @@ export default function Dashboards({
|
|||||||
<div className="dashboard-item-name-container home-data-item-name-container">
|
<div className="dashboard-item-name-container home-data-item-name-container">
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
dashboard.id % 2 === 0
|
Math.random() % 2 === 0
|
||||||
? '/Icons/eight-ball.svg'
|
? '/Icons/eight-ball.svg'
|
||||||
: '/Icons/circus-tent.svg'
|
: '/Icons/circus-tent.svg'
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { TableProps } from 'antd/lib';
|
import { TableProps } from 'antd/lib';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/v1/dashboards/create';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
@ -63,6 +63,7 @@ import {
|
|||||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
@ -83,6 +84,7 @@ import {
|
|||||||
WidgetRow,
|
WidgetRow,
|
||||||
Widgets,
|
Widgets,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||||
import ImportJSON from './ImportJSON';
|
import ImportJSON from './ImportJSON';
|
||||||
@ -226,7 +228,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredDashboards = filterDashboard(
|
const filteredDashboards = filterDashboard(
|
||||||
searchString,
|
searchString,
|
||||||
dashboardListResponse || [],
|
dashboardListResponse?.data || [],
|
||||||
);
|
);
|
||||||
if (sortOrder.columnKey === 'updatedAt') {
|
if (sortOrder.columnKey === 'updatedAt') {
|
||||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||||
@ -256,17 +258,19 @@ function DashboardsList(): JSX.Element {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
const data: Data[] =
|
const data: Data[] =
|
||||||
dashboards?.map((e) => ({
|
dashboards?.map((e) => ({
|
||||||
createdAt: e.createdAt,
|
createdAt: e.createdAt,
|
||||||
description: e.data.description || '',
|
description: e.data.description || '',
|
||||||
id: e.uuid,
|
id: e.id,
|
||||||
lastUpdatedTime: e.updatedAt,
|
lastUpdatedTime: e.updatedAt,
|
||||||
name: e.data.title,
|
name: e.data.title,
|
||||||
tags: e.data.tags || [],
|
tags: e.data.tags || [],
|
||||||
key: e.uuid,
|
key: e.id,
|
||||||
createdBy: e.createdBy,
|
createdBy: e.createdBy,
|
||||||
isLocked: !!e.isLocked || false,
|
isLocked: !!e.locked || false,
|
||||||
lastUpdatedBy: e.updatedBy,
|
lastUpdatedBy: e.updatedBy,
|
||||||
image: e.data.image || Base64Icons[0],
|
image: e.data.image || Base64Icons[0],
|
||||||
variables: e.data.variables,
|
variables: e.data.variables,
|
||||||
@ -292,28 +296,20 @@ function DashboardsList(): JSX.Element {
|
|||||||
version: ENTITY_VERSION_V4,
|
version: ENTITY_VERSION_V4,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
safeNavigate(
|
||||||
safeNavigate(
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
dashboardId: response.data.id,
|
||||||
dashboardId: response.payload.uuid,
|
}),
|
||||||
}),
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setNewDashboardState({
|
|
||||||
...newDashboardState,
|
|
||||||
loading: false,
|
|
||||||
error: true,
|
|
||||||
errorMessage: response.error || 'Something went wrong',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
setNewDashboardState({
|
setNewDashboardState({
|
||||||
...newDashboardState,
|
...newDashboardState,
|
||||||
error: true,
|
error: true,
|
||||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [newDashboardState, safeNavigate, t]);
|
}, [newDashboardState, safeNavigate, showErrorModal, t]);
|
||||||
|
|
||||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||||
logEvent('Dashboard List: Import JSON clicked', {});
|
logEvent('Dashboard List: Import JSON clicked', {});
|
||||||
@ -327,7 +323,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||||
const filteredDashboards = filterDashboard(
|
const filteredDashboards = filterDashboard(
|
||||||
searchText,
|
searchText,
|
||||||
dashboardListResponse || [],
|
dashboardListResponse?.data || [],
|
||||||
);
|
);
|
||||||
setDashboards(filteredDashboards);
|
setDashboards(filteredDashboards);
|
||||||
setIsFilteringDashboards(false);
|
setIsFilteringDashboards(false);
|
||||||
@ -677,7 +673,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
!isUndefined(dashboardListResponse)
|
!isUndefined(dashboardListResponse)
|
||||||
) {
|
) {
|
||||||
logEvent('Dashboard List: Page visited', {
|
logEvent('Dashboard List: Page visited', {
|
||||||
number: dashboardListResponse?.length,
|
number: dashboardListResponse?.data?.length,
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
}
|
}
|
||||||
|
@ -14,19 +14,21 @@ import {
|
|||||||
UploadProps,
|
UploadProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
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 ROUTES from 'constants/routes';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||||
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
|
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/
|
// #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
|
// See more: https://github.com/lucide-icons/lucide/issues/94
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
function ImportJSON({
|
function ImportJSON({
|
||||||
isImportJSONModalVisible,
|
isImportJSONModalVisible,
|
||||||
@ -74,6 +76,8 @@ function ImportJSON({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
const onClickLoadJsonHandler = async (): Promise<void> => {
|
const onClickLoadJsonHandler = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setDashboardCreating(true);
|
setDashboardCreating(true);
|
||||||
@ -81,11 +85,6 @@ function ImportJSON({
|
|||||||
|
|
||||||
const dashboardData = JSON.parse(editorValue) as DashboardData;
|
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) {
|
if (dashboardData?.layout) {
|
||||||
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
|
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
|
||||||
} else {
|
} else {
|
||||||
@ -97,28 +96,19 @@ function ImportJSON({
|
|||||||
uploadedGrafana,
|
uploadedGrafana,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
safeNavigate(
|
||||||
safeNavigate(
|
generatePath(ROUTES.DASHBOARD, {
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
dashboardId: response.data.id,
|
||||||
dashboardId: response.payload.uuid,
|
}),
|
||||||
}),
|
);
|
||||||
);
|
logEvent('Dashboard List: New dashboard imported successfully', {
|
||||||
logEvent('Dashboard List: New dashboard imported successfully', {
|
dashboardId: response.data?.id,
|
||||||
dashboardId: response.payload?.uuid,
|
dashboardName: response.data?.data?.title,
|
||||||
dashboardName: response.payload?.data?.title,
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setIsCreateDashboardError(true);
|
|
||||||
notifications.error({
|
|
||||||
message:
|
|
||||||
response.error ||
|
|
||||||
t('something_went_wrong', {
|
|
||||||
ns: 'common',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setDashboardCreating(false);
|
setDashboardCreating(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
setDashboardCreating(false);
|
setDashboardCreating(false);
|
||||||
setIsCreateDashboardError(true);
|
setIsCreateDashboardError(true);
|
||||||
notifications.error({
|
notifications.error({
|
||||||
|
@ -6,8 +6,7 @@ import { executeSearchQueries } from '../utils';
|
|||||||
|
|
||||||
describe('executeSearchQueries', () => {
|
describe('executeSearchQueries', () => {
|
||||||
const firstDashboard: Dashboard = {
|
const firstDashboard: Dashboard = {
|
||||||
id: 11111,
|
id: uuid(),
|
||||||
uuid: uuid(),
|
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
createdBy: '',
|
createdBy: '',
|
||||||
@ -18,8 +17,7 @@ describe('executeSearchQueries', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const secondDashboard: Dashboard = {
|
const secondDashboard: Dashboard = {
|
||||||
id: 22222,
|
id: uuid(),
|
||||||
uuid: uuid(),
|
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
createdBy: '',
|
createdBy: '',
|
||||||
@ -30,8 +28,7 @@ describe('executeSearchQueries', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const thirdDashboard: Dashboard = {
|
const thirdDashboard: Dashboard = {
|
||||||
id: 333333,
|
id: uuid(),
|
||||||
uuid: uuid(),
|
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
createdBy: '',
|
createdBy: '',
|
||||||
|
@ -59,7 +59,7 @@ export function DeleteButton({
|
|||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteDashboardMutation.mutateAsync(undefined, {
|
deleteDashboardMutation.mutate(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: t('dashboard:delete_dashboard_success', {
|
message: t('dashboard:delete_dashboard_success', {
|
||||||
|
@ -14,7 +14,7 @@ export const generateSearchData = (
|
|||||||
|
|
||||||
dashboards.forEach((dashboard) => {
|
dashboards.forEach((dashboard) => {
|
||||||
dashboardSearchData.push({
|
dashboardSearchData.push({
|
||||||
id: dashboard.uuid,
|
id: dashboard.id,
|
||||||
title: dashboard.data.title,
|
title: dashboard.data.title,
|
||||||
description: dashboard.data.description,
|
description: dashboard.data.description,
|
||||||
tags: dashboard.data.tags || [],
|
tags: dashboard.data.tags || [],
|
||||||
|
@ -21,8 +21,10 @@ import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
|||||||
|
|
||||||
import { DataType } from '../TableView';
|
import { DataType } from '../TableView';
|
||||||
import {
|
import {
|
||||||
|
escapeHtml,
|
||||||
filterKeyForField,
|
filterKeyForField,
|
||||||
jsonToDataNodes,
|
jsonToDataNodes,
|
||||||
|
parseFieldValue,
|
||||||
recursiveParseJSON,
|
recursiveParseJSON,
|
||||||
removeEscapeCharacters,
|
removeEscapeCharacters,
|
||||||
unescapeString,
|
unescapeString,
|
||||||
@ -85,7 +87,7 @@ export function TableViewActions(
|
|||||||
record.field === 'body'
|
record.field === 'body'
|
||||||
? {
|
? {
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(unescapeString(record.value), {
|
dompurify.sanitize(unescapeString(escapeHtml(record.value)), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -155,7 +157,11 @@ export function TableViewActions(
|
|||||||
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={onClickHandler(OPERATORS['='], fieldFilterKey, fieldData.value)}
|
onClick={onClickHandler(
|
||||||
|
OPERATORS['='],
|
||||||
|
fieldFilterKey,
|
||||||
|
parseFieldValue(fieldData.value),
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Filter out value">
|
<Tooltip title="Filter out value">
|
||||||
@ -171,7 +177,7 @@ export function TableViewActions(
|
|||||||
onClick={onClickHandler(
|
onClick={onClickHandler(
|
||||||
OPERATORS['!='],
|
OPERATORS['!='],
|
||||||
fieldFilterKey,
|
fieldFilterKey,
|
||||||
fieldData.value,
|
parseFieldValue(fieldData.value),
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -259,6 +259,24 @@ export const getDataTypes = (value: unknown): DataTypes => {
|
|||||||
return determineType(value);
|
return determineType(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prevent html rendering in the value
|
||||||
|
export const escapeHtml = (unsafe: string): string =>
|
||||||
|
unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
// parse field value to remove escaping characters
|
||||||
|
export const parseFieldValue = (value: string): string => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
|
// 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
|
// the log line readable
|
||||||
export const removeEscapeCharacters = (str: string): string =>
|
export const removeEscapeCharacters = (str: string): string =>
|
||||||
|
@ -28,16 +28,12 @@ import LogsExplorerTable from 'container/LogsExplorerTable';
|
|||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|
||||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
|
||||||
import useClickOutside from 'hooks/useClickOutside';
|
import useClickOutside from 'hooks/useClickOutside';
|
||||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
@ -98,7 +94,6 @@ function LogsExplorerViews({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
chartQueryKeyRef: MutableRefObject<any>;
|
chartQueryKeyRef: MutableRefObject<any>;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
// 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 [queryId, setQueryId] = useState<string>(v4());
|
||||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||||
|
|
||||||
const handleAxisError = useAxiosError();
|
|
||||||
|
|
||||||
const listQuery = useMemo(() => {
|
const listQuery = useMemo(() => {
|
||||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||||
|
|
||||||
@ -396,11 +389,6 @@ function LogsExplorerViews({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data?.payload]);
|
}, [data?.payload]);
|
||||||
|
|
||||||
const {
|
|
||||||
mutate: updateDashboard,
|
|
||||||
isLoading: isUpdateDashboardLoading,
|
|
||||||
} = useUpdateDashboard();
|
|
||||||
|
|
||||||
const getUpdatedQueryForExport = useCallback((): Query => {
|
const getUpdatedQueryForExport = useCallback((): Query => {
|
||||||
const updatedQuery = cloneDeep(currentQuery);
|
const updatedQuery = cloneDeep(currentQuery);
|
||||||
|
|
||||||
@ -424,68 +412,22 @@ function LogsExplorerViews({
|
|||||||
? getUpdatedQueryForExport()
|
? getUpdatedQueryForExport()
|
||||||
: exportDefaultQuery;
|
: exportDefaultQuery;
|
||||||
|
|
||||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
|
||||||
dashboard,
|
|
||||||
query,
|
|
||||||
widgetId,
|
|
||||||
panelTypeParam,
|
|
||||||
options.selectColumns,
|
|
||||||
);
|
|
||||||
|
|
||||||
logEvent('Logs Explorer: Add to dashboard successful', {
|
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||||
panelType,
|
panelType,
|
||||||
isNewDashboard,
|
isNewDashboard,
|
||||||
dashboardName: dashboard?.data?.title,
|
dashboardName: dashboard?.data?.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateDashboard(updatedDashboard, {
|
const dashboardEditView = generateExportToDashboardLink({
|
||||||
onSuccess: (data) => {
|
query,
|
||||||
if (data.error) {
|
panelType: panelTypeParam,
|
||||||
const message =
|
dashboardId: dashboard.id,
|
||||||
data.error === 'feature usage exceeded' ? (
|
widgetId,
|
||||||
<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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
safeNavigate(dashboardEditView);
|
||||||
},
|
},
|
||||||
[
|
[getUpdatedQueryForExport, exportDefaultQuery, safeNavigate, panelType],
|
||||||
getUpdatedQueryForExport,
|
|
||||||
exportDefaultQuery,
|
|
||||||
options.selectColumns,
|
|
||||||
safeNavigate,
|
|
||||||
notifications,
|
|
||||||
panelType,
|
|
||||||
updateDashboard,
|
|
||||||
handleAxisError,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -811,7 +753,6 @@ function LogsExplorerViews({
|
|||||||
<ExplorerOptionWrapper
|
<ExplorerOptionWrapper
|
||||||
disabled={!stagedQuery}
|
disabled={!stagedQuery}
|
||||||
query={exportDefaultQuery}
|
query={exportDefaultQuery}
|
||||||
isLoading={isUpdateDashboardLoading}
|
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
sourcepage={DataSource.LOGS}
|
sourcepage={DataSource.LOGS}
|
||||||
/>
|
/>
|
||||||
|
@ -2,18 +2,12 @@ import './Explorer.styles.scss';
|
|||||||
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Switch } from 'antd';
|
import { Switch } from 'antd';
|
||||||
import axios from 'axios';
|
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
|
||||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
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 { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@ -39,13 +33,6 @@ function Explorer(): JSX.Element {
|
|||||||
currentQuery,
|
currentQuery,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
const { safeNavigate } = useSafeNavigate();
|
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 [searchParams, setSearchParams] = useSearchParams();
|
||||||
const isOneChartPerQueryEnabled =
|
const isOneChartPerQueryEnabled =
|
||||||
@ -86,59 +73,16 @@ function Explorer(): JSX.Element {
|
|||||||
|
|
||||||
const widgetId = uuid();
|
const widgetId = uuid();
|
||||||
|
|
||||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
const dashboardEditView = generateExportToDashboardLink({
|
||||||
dashboard,
|
query: queryToExport || exportDefaultQuery,
|
||||||
queryToExport || exportDefaultQuery,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
dashboardId: dashboard.id,
|
||||||
widgetId,
|
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, safeNavigate],
|
||||||
[exportDefaultQuery, notifications, updateDashboard],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const splitedQueries = useMemo(
|
const splitedQueries = useMemo(
|
||||||
@ -201,7 +145,6 @@ function Explorer(): JSX.Element {
|
|||||||
<ExplorerOptionWrapper
|
<ExplorerOptionWrapper
|
||||||
disabled={!stagedQuery}
|
disabled={!stagedQuery}
|
||||||
query={exportDefaultQuery}
|
query={exportDefaultQuery}
|
||||||
isLoading={isLoading}
|
|
||||||
sourcepage={DataSource.METRICS}
|
sourcepage={DataSource.METRICS}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
isOneChartPerQuery={showOneChartPerQuery}
|
isOneChartPerQuery={showOneChartPerQuery}
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -44,11 +43,8 @@ import { FullScreenHandle } from 'react-full-screen';
|
|||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import {
|
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
Dashboard,
|
import { Props } from 'types/api/dashboard/update';
|
||||||
DashboardData,
|
|
||||||
IDashboardVariable,
|
|
||||||
} from 'types/api/dashboard/getAll';
|
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
@ -65,10 +61,9 @@ interface DashboardDescriptionProps {
|
|||||||
|
|
||||||
export function sanitizeDashboardData(
|
export function sanitizeDashboardData(
|
||||||
selectedData: DashboardData,
|
selectedData: DashboardData,
|
||||||
): Omit<DashboardData, 'uuid'> {
|
): DashboardData {
|
||||||
if (!selectedData?.variables) {
|
if (!selectedData?.variables) {
|
||||||
const { uuid, ...rest } = selectedData;
|
return selectedData;
|
||||||
return rest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||||
@ -80,9 +75,8 @@ export function sanitizeDashboardData(
|
|||||||
{} as Record<string, IDashboardVariable>,
|
{} as Record<string, IDashboardVariable>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { uuid, ...restData } = selectedData;
|
|
||||||
return {
|
return {
|
||||||
...restData,
|
...selectedData,
|
||||||
variables: updatedVariables,
|
variables: updatedVariables,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -108,7 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
const selectedData = selectedDashboard
|
const selectedData = selectedDashboard
|
||||||
? {
|
? {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
uuid: selectedDashboard.uuid,
|
uuid: selectedDashboard.id,
|
||||||
}
|
}
|
||||||
: ({} as DashboardData);
|
: ({} as DashboardData);
|
||||||
|
|
||||||
@ -162,7 +156,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
setSelectedRowWidgetId(null);
|
setSelectedRowWidgetId(null);
|
||||||
handleToggleDashboardSlider(true);
|
handleToggleDashboardSlider(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
@ -178,8 +172,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
if (!selectedDashboard) {
|
if (!selectedDashboard) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updatedDashboard = {
|
const updatedDashboard: Props = {
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
title: updatedTitle,
|
title: updatedTitle,
|
||||||
@ -191,13 +186,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
message: 'Dashboard renamed successfully',
|
message: 'Dashboard renamed successfully',
|
||||||
});
|
});
|
||||||
setIsRenameDashboardOpen(false);
|
setIsRenameDashboardOpen(false);
|
||||||
if (updatedDashboard.payload)
|
if (updatedDashboard.data) setSelectedDashboard(updatedDashboard.data);
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
notifications.error({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
setIsRenameDashboardOpen(true);
|
setIsRenameDashboardOpen(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -251,8 +242,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedDashboard: Dashboard = {
|
const updatedDashboard: Props = {
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
layout: [
|
layout: [
|
||||||
@ -279,28 +271,21 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
uuid: selectedDashboard.uuid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updateDashboardMutation.mutate(updatedDashboard, {
|
updateDashboardMutation.mutate(updatedDashboard, {
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (updatedDashboard.payload) {
|
if (updatedDashboard.data) {
|
||||||
if (updatedDashboard.payload.data.layout)
|
if (updatedDashboard.data.data.layout)
|
||||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
setLayouts(sortLayout(updatedDashboard.data.data.layout));
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsPanelNameModalOpen(false);
|
setIsPanelNameModalOpen(false);
|
||||||
setSectionName(DEFAULT_ROW_NAME);
|
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
|
<DeleteButton
|
||||||
createdBy={selectedDashboard?.createdBy || ''}
|
createdBy={selectedDashboard?.createdBy || ''}
|
||||||
name={selectedDashboard?.data.title || ''}
|
name={selectedDashboard?.data.title || ''}
|
||||||
id={String(selectedDashboard?.uuid) || ''}
|
id={String(selectedDashboard?.id) || ''}
|
||||||
isLocked={isDashboardLocked}
|
isLocked={isDashboardLocked}
|
||||||
routeToListPage
|
routeToListPage
|
||||||
/>
|
/>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import './GeneralSettings.styles.scss';
|
import './GeneralSettings.styles.scss';
|
||||||
|
|
||||||
import { Col, Input, Select, Space, Typography } from 'antd';
|
import { Col, Input, Select, Space, Typography } from 'antd';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
@ -38,14 +36,12 @@ function GeneralDashboardSettings(): JSX.Element {
|
|||||||
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const onSaveHandler = (): void => {
|
const onSaveHandler = (): void => {
|
||||||
if (!selectedDashboard) return;
|
if (!selectedDashboard) return;
|
||||||
|
|
||||||
updateDashboardMutation.mutateAsync(
|
updateDashboardMutation.mutate(
|
||||||
{
|
{
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
description: updatedDescription,
|
description: updatedDescription,
|
||||||
@ -56,15 +52,11 @@ function GeneralDashboardSettings(): JSX.Element {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (updatedDashboard.payload) {
|
if (updatedDashboard.data) {
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {},
|
||||||
notifications.error({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -171,7 +171,8 @@ function VariablesSetting({
|
|||||||
|
|
||||||
updateMutation.mutateAsync(
|
updateMutation.mutateAsync(
|
||||||
{
|
{
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
variables: updatedVariablesData,
|
variables: updatedVariablesData,
|
||||||
@ -179,18 +180,13 @@ function VariablesSetting({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (updatedDashboard) => {
|
onSuccess: (updatedDashboard) => {
|
||||||
if (updatedDashboard.payload) {
|
if (updatedDashboard.data) {
|
||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: t('variable_updated_successfully'),
|
message: t('variable_updated_successfully'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
|
||||||
notifications.error({
|
|
||||||
message: t('error_while_updating_variable'),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -127,7 +127,7 @@ function QuerySection({
|
|||||||
panelType: selectedWidget.panelTypes,
|
panelType: selectedWidget.panelTypes,
|
||||||
queryType: currentQuery.queryType,
|
queryType: currentQuery.queryType,
|
||||||
widgetId: selectedWidget.id,
|
widgetId: selectedWidget.id,
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
isNewPanel,
|
isNewPanel,
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,6 @@ import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
|||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
@ -41,10 +40,10 @@ import { AppState } from 'store/reducers';
|
|||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
ColumnUnit,
|
ColumnUnit,
|
||||||
Dashboard,
|
|
||||||
LegendPosition,
|
LegendPosition,
|
||||||
Widgets,
|
Widgets,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
|
import { Props } from 'types/api/dashboard/update';
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
@ -141,7 +140,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
if (!logEventCalledRef.current) {
|
if (!logEventCalledRef.current) {
|
||||||
logEvent('Panel Edit: Page visited', {
|
logEvent('Panel Edit: Page visited', {
|
||||||
panelType: selectedWidget?.panelTypes,
|
panelType: selectedWidget?.panelTypes,
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
widgetId: selectedWidget?.id,
|
widgetId: selectedWidget?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
isNewPanel: !!isWidgetNotPresent,
|
isNewPanel: !!isWidgetNotPresent,
|
||||||
@ -345,8 +344,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
return { selectedWidget, preWidgets, afterWidgets };
|
return { selectedWidget, preWidgets, afterWidgets };
|
||||||
}, [selectedDashboard, query]);
|
}, [selectedDashboard, query]);
|
||||||
|
|
||||||
const handleError = useAxiosError();
|
|
||||||
|
|
||||||
// this loading state is to take care of mismatch in the responses for table and other panels
|
// 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
|
// hence while changing the query contains the older value and the processing logic fails
|
||||||
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
||||||
@ -470,9 +467,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
updatedLayout = newLayoutItem;
|
updatedLayout = newLayoutItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboard: Dashboard = {
|
const dashboard: Props = {
|
||||||
...selectedDashboard,
|
id: selectedDashboard.id,
|
||||||
uuid: selectedDashboard.uuid,
|
|
||||||
data: {
|
data: {
|
||||||
...selectedDashboard.data,
|
...selectedDashboard.data,
|
||||||
widgets: isNewDashboard
|
widgets: isNewDashboard
|
||||||
@ -540,15 +537,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateDashboardMutation.mutateAsync(dashboard, {
|
updateDashboardMutation.mutateAsync(dashboard, {
|
||||||
onSuccess: () => {
|
onSuccess: (updatedDashboard) => {
|
||||||
setSelectedRowWidgetId(null);
|
setSelectedRowWidgetId(null);
|
||||||
setSelectedDashboard(dashboard);
|
setSelectedDashboard(updatedDashboard.data);
|
||||||
setToScrollWidgetId(selectedWidget?.id || '');
|
setToScrollWidgetId(selectedWidget?.id || '');
|
||||||
safeNavigate({
|
safeNavigate({
|
||||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: handleError,
|
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
@ -562,7 +558,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
currentQuery,
|
currentQuery,
|
||||||
preWidgets,
|
preWidgets,
|
||||||
updateDashboardMutation,
|
updateDashboardMutation,
|
||||||
handleError,
|
|
||||||
widgets,
|
widgets,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
setToScrollWidgetId,
|
setToScrollWidgetId,
|
||||||
@ -601,7 +596,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
|
|
||||||
logEvent('Panel Edit: Save changes', {
|
logEvent('Panel Edit: Save changes', {
|
||||||
panelType: selectedWidget.panelTypes,
|
panelType: selectedWidget.panelTypes,
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
widgetId: selectedWidget.id,
|
widgetId: selectedWidget.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
queryType: currentQuery.queryType,
|
queryType: currentQuery.queryType,
|
||||||
|
@ -26,6 +26,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
import { checkVersionState } from 'utils/app';
|
import { checkVersionState } from 'utils/app';
|
||||||
@ -301,10 +302,11 @@ function SideNav(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isOnBasicPlan =
|
const isOnBasicPlan =
|
||||||
activeLicenseFetchError &&
|
(activeLicenseFetchError &&
|
||||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||||
activeLicenseFetchError?.getHttpStatusCode(),
|
activeLicenseFetchError?.getHttpStatusCode(),
|
||||||
);
|
)) ||
|
||||||
|
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
|
||||||
|
|
||||||
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
|
if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) {
|
||||||
updatedMenuItems = updatedMenuItems.filter(
|
updatedMenuItems = updatedMenuItems.filter(
|
||||||
@ -353,6 +355,7 @@ function SideNav(): JSX.Element {
|
|||||||
t,
|
t,
|
||||||
user.role,
|
user.role,
|
||||||
activeLicenseFetchError,
|
activeLicenseFetchError,
|
||||||
|
activeLicense?.status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -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 { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useMutation, UseMutationResult } from 'react-query';
|
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 = (
|
export const useDeleteDashboard = (
|
||||||
id: string,
|
id: string,
|
||||||
): UseMutationResult<PayloadProps, unknown, void, unknown> =>
|
): UseMutationResult<SuccessResponseV2<null>, APIError, void, unknown> => {
|
||||||
useMutation({
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
|
return useMutation<SuccessResponseV2<null>, APIError>({
|
||||||
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
|
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
deleteDashboard({
|
deleteDashboard({
|
||||||
uuid: id,
|
id,
|
||||||
}),
|
}),
|
||||||
|
onError: (error: APIError) => {
|
||||||
|
showErrorModal(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
@ -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 { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useQuery, UseQueryResult } from 'react-query';
|
import { useQuery, UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
export const useGetAllDashboard = (): UseQueryResult<Dashboard[], unknown> =>
|
export const useGetAllDashboard = (): UseQueryResult<
|
||||||
useQuery<Dashboard[]>({
|
SuccessResponseV2<Dashboard[]>,
|
||||||
queryFn: getAllDashboardList,
|
APIError
|
||||||
|
> => {
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
|
return useQuery<SuccessResponseV2<Dashboard[]>, APIError>({
|
||||||
|
queryFn: getAll,
|
||||||
|
onError: (error) => {
|
||||||
|
showErrorModal(error);
|
||||||
|
},
|
||||||
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
|
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import update from 'api/dashboard/update';
|
import update from 'api/v1/dashboards/id/update';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useMutation, UseMutationResult } from 'react-query';
|
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 { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { Props } from 'types/api/dashboard/update';
|
import { Props } from 'types/api/dashboard/update';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
export const useUpdateDashboard = (): UseUpdateDashboard => {
|
export const useUpdateDashboard = (): UseUpdateDashboard => {
|
||||||
const { updatedTimeRef } = useDashboard();
|
const { updatedTimeRef } = useDashboard();
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
return useMutation(update, {
|
return useMutation(update, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.payload) {
|
if (data.data) {
|
||||||
updatedTimeRef.current = dayjs(data.payload.updatedAt);
|
updatedTimeRef.current = dayjs(data.data.updatedAt);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showErrorModal(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
type UseUpdateDashboard = UseMutationResult<
|
type UseUpdateDashboard = UseMutationResult<
|
||||||
SuccessResponse<Dashboard> | ErrorResponse,
|
SuccessResponseV2<Dashboard>,
|
||||||
unknown,
|
APIError,
|
||||||
Props,
|
Props,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
@ -38,7 +38,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
|||||||
logEvent('Panel Edit: Create alert', {
|
logEvent('Panel Edit: Create alert', {
|
||||||
panelType: widget.panelTypes,
|
panelType: widget.panelTypes,
|
||||||
dashboardName: selectedDashboard?.data?.title,
|
dashboardName: selectedDashboard?.data?.title,
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
widgetId: widget.id,
|
widgetId: widget.id,
|
||||||
queryType: widget.query.queryType,
|
queryType: widget.query.queryType,
|
||||||
});
|
});
|
||||||
@ -47,7 +47,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
|||||||
action: MenuItemKeys.CreateAlerts,
|
action: MenuItemKeys.CreateAlerts,
|
||||||
panelType: widget.panelTypes,
|
panelType: widget.panelTypes,
|
||||||
dashboardName: selectedDashboard?.data?.title,
|
dashboardName: selectedDashboard?.data?.title,
|
||||||
dashboardId: selectedDashboard?.uuid,
|
dashboardId: selectedDashboard?.id,
|
||||||
widgetId: widget.id,
|
widgetId: widget.id,
|
||||||
queryType: widget.query.queryType,
|
queryType: widget.query.queryType,
|
||||||
});
|
});
|
||||||
|
@ -3,8 +3,7 @@ export const dashboardSuccessResponse = {
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: '1',
|
||||||
uuid: '1',
|
|
||||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||||
createdBy: null,
|
createdBy: null,
|
||||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||||
@ -23,8 +22,7 @@ export const dashboardSuccessResponse = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: '2',
|
||||||
uuid: '2',
|
|
||||||
createdAt: '2022-11-16T13:20:47.064874419Z',
|
createdAt: '2022-11-16T13:20:47.064874419Z',
|
||||||
createdBy: null,
|
createdBy: null,
|
||||||
updatedAt: '2024-05-21T06:42:30.546630961Z',
|
updatedAt: '2024-05-21T06:42:30.546630961Z',
|
||||||
@ -53,8 +51,7 @@ export const dashboardEmptyState = {
|
|||||||
export const getDashboardById = {
|
export const getDashboardById = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: '1',
|
||||||
uuid: '1',
|
|
||||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||||
createdBy: 'integration',
|
createdBy: 'integration',
|
||||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||||
@ -78,8 +75,7 @@ export const getDashboardById = {
|
|||||||
export const getNonIntegrationDashboardById = {
|
export const getNonIntegrationDashboardById = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: '1',
|
||||||
uuid: '1',
|
|
||||||
createdAt: '2022-11-16T13:29:47.064874419Z',
|
createdAt: '2022-11-16T13:29:47.064874419Z',
|
||||||
createdBy: 'thor',
|
createdBy: 'thor',
|
||||||
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
updatedAt: '2024-05-21T06:41:30.546630961Z',
|
||||||
|
@ -234,7 +234,6 @@ describe('dashboard list page', () => {
|
|||||||
const firstDashboardData = dashboardSuccessResponse.data[0];
|
const firstDashboardData = dashboardSuccessResponse.data[0];
|
||||||
expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith(
|
expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: firstDashboardData.uuid,
|
|
||||||
title: firstDashboardData.data.title,
|
title: firstDashboardData.data.title,
|
||||||
createdAt: firstDashboardData.createdAt,
|
createdAt: firstDashboardData.createdAt,
|
||||||
}),
|
}),
|
||||||
|
@ -19,9 +19,9 @@ function DashboardPage(): JSX.Element {
|
|||||||
: 'Something went wrong';
|
: 'Something went wrong';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dashboardTitle = dashboardResponse.data?.data.title;
|
const dashboardTitle = dashboardResponse.data?.data.data.title;
|
||||||
document.title = dashboardTitle || document.title;
|
document.title = dashboardTitle || document.title;
|
||||||
}, [dashboardResponse.data?.data.title, isFetching]);
|
}, [dashboardResponse.data?.data.data.title, isFetching]);
|
||||||
|
|
||||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||||
return <NotFound />;
|
return <NotFound />;
|
||||||
|
@ -4,7 +4,6 @@ import { FilterOutlined } from '@ant-design/icons';
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Button, Card, Tabs, Tooltip } from 'antd';
|
import { Button, Card, Tabs, Tooltip } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import axios from 'axios';
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
@ -19,13 +18,10 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
|
|||||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
import { defaultSelectedColumns } from 'container/TracesExplorer/ListView/configs';
|
||||||
import QuerySection from 'container/TracesExplorer/QuerySection';
|
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 { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import { cloneDeep, isEmpty, set } from 'lodash-es';
|
import { cloneDeep, isEmpty, set } from 'lodash-es';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
@ -40,8 +36,6 @@ import { ActionsWrapper, Container } from './styles';
|
|||||||
import { getTabsItems } from './utils';
|
import { getTabsItems } from './utils';
|
||||||
|
|
||||||
function TracesExplorer(): JSX.Element {
|
function TracesExplorer(): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
panelType,
|
panelType,
|
||||||
@ -124,9 +118,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
[currentQuery, updateAllQueriesOperators],
|
[currentQuery, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
|
const getUpdatedQueryForExport = useCallback((): Query => {
|
||||||
|
|
||||||
const getUpdatedQueryForExport = (): Query => {
|
|
||||||
const updatedQuery = cloneDeep(currentQuery);
|
const updatedQuery = cloneDeep(currentQuery);
|
||||||
|
|
||||||
set(
|
set(
|
||||||
@ -136,7 +128,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return updatedQuery;
|
return updatedQuery;
|
||||||
};
|
}, [currentQuery, options.selectColumns]);
|
||||||
|
|
||||||
const handleExport = useCallback(
|
const handleExport = useCallback(
|
||||||
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
(dashboard: Dashboard | null, isNewDashboard?: boolean): void => {
|
||||||
@ -153,65 +145,22 @@ function TracesExplorer(): JSX.Element {
|
|||||||
? getUpdatedQueryForExport()
|
? getUpdatedQueryForExport()
|
||||||
: exportDefaultQuery;
|
: exportDefaultQuery;
|
||||||
|
|
||||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
|
||||||
dashboard,
|
|
||||||
query,
|
|
||||||
widgetId,
|
|
||||||
panelTypeParam,
|
|
||||||
options.selectColumns,
|
|
||||||
);
|
|
||||||
|
|
||||||
logEvent('Traces Explorer: Add to dashboard successful', {
|
logEvent('Traces Explorer: Add to dashboard successful', {
|
||||||
panelType,
|
panelType,
|
||||||
isNewDashboard,
|
isNewDashboard,
|
||||||
dashboardName: dashboard?.data?.title,
|
dashboardName: dashboard?.data?.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateDashboard(updatedDashboard, {
|
const dashboardEditView = generateExportToDashboardLink({
|
||||||
onSuccess: (data) => {
|
query,
|
||||||
if (data.error) {
|
panelType: panelTypeParam,
|
||||||
const message =
|
dashboardId: dashboard.id,
|
||||||
data.error === 'feature usage exceeded' ? (
|
widgetId,
|
||||||
<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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
safeNavigate(dashboardEditView);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[exportDefaultQuery, panelType, safeNavigate, getUpdatedQueryForExport],
|
||||||
[exportDefaultQuery, notifications, panelType, updateDashboard],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useShareBuilderUrl(defaultQuery);
|
useShareBuilderUrl(defaultQuery);
|
||||||
@ -282,11 +231,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
|
|
||||||
<Container className="traces-explorer-views">
|
<Container className="traces-explorer-views">
|
||||||
<ActionsWrapper>
|
<ActionsWrapper>
|
||||||
<ExportPanel
|
<ExportPanel query={exportDefaultQuery} onExport={handleExport} />
|
||||||
query={exportDefaultQuery}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onExport={handleExport}
|
|
||||||
/>
|
|
||||||
</ActionsWrapper>
|
</ActionsWrapper>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
@ -299,7 +244,6 @@ function TracesExplorer(): JSX.Element {
|
|||||||
<ExplorerOptionWrapper
|
<ExplorerOptionWrapper
|
||||||
disabled={!stagedQuery}
|
disabled={!stagedQuery}
|
||||||
query={exportDefaultQuery}
|
query={exportDefaultQuery}
|
||||||
isLoading={isLoading}
|
|
||||||
sourcepage={DataSource.TRACES}
|
sourcepage={DataSource.TRACES}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
/>
|
/>
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import getDashboard from 'api/dashboard/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
import locked from 'api/v1/dashboards/id/lock';
|
||||||
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
|
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useTabVisibility from 'hooks/useTabFocus';
|
import useTabVisibility from 'hooks/useTabFocus';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
@ -18,6 +16,7 @@ import isEqual from 'lodash-es/isEqual';
|
|||||||
import isUndefined from 'lodash-es/isUndefined';
|
import isUndefined from 'lodash-es/isUndefined';
|
||||||
import omitBy from 'lodash-es/omitBy';
|
import omitBy from 'lodash-es/omitBy';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
@ -36,7 +35,9 @@ import { Dispatch } from 'redux';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||||
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
@ -52,7 +53,10 @@ const DashboardContext = createContext<IDashboardContext>({
|
|||||||
isDashboardLocked: false,
|
isDashboardLocked: false,
|
||||||
handleToggleDashboardSlider: () => {},
|
handleToggleDashboardSlider: () => {},
|
||||||
handleDashboardLockToggle: () => {},
|
handleDashboardLockToggle: () => {},
|
||||||
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
|
dashboardResponse: {} as UseQueryResult<
|
||||||
|
SuccessResponseV2<Dashboard>,
|
||||||
|
APIError
|
||||||
|
>,
|
||||||
selectedDashboard: {} as Dashboard,
|
selectedDashboard: {} as Dashboard,
|
||||||
dashboardId: '',
|
dashboardId: '',
|
||||||
layouts: [],
|
layouts: [],
|
||||||
@ -116,6 +120,8 @@ export function DashboardProvider({
|
|||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
|
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
|
||||||
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
|
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
|
||||||
|
|
||||||
@ -270,18 +276,24 @@ export function DashboardProvider({
|
|||||||
setIsDashboardFetching(true);
|
setIsDashboardFetching(true);
|
||||||
try {
|
try {
|
||||||
return await getDashboard({
|
return await getDashboard({
|
||||||
uuid: dashboardId,
|
id: dashboardId,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setIsDashboardFetching(false);
|
setIsDashboardFetching(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
onSuccess: (data) => {
|
onError: (error) => {
|
||||||
const updatedDashboardData = transformDashboardVariables(data);
|
showErrorModal(error as APIError);
|
||||||
|
},
|
||||||
|
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||||
|
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||||
const updatedDate = dayjs(updatedDashboardData.updatedAt);
|
const updatedDate = dayjs(updatedDashboardData.updatedAt);
|
||||||
|
|
||||||
setIsDashboardLocked(updatedDashboardData?.isLocked || false);
|
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||||
|
|
||||||
// on first render
|
// on first render
|
||||||
if (updatedTimeRef.current === null) {
|
if (updatedTimeRef.current === null) {
|
||||||
@ -387,29 +399,25 @@ export function DashboardProvider({
|
|||||||
setIsDashboardSlider(value);
|
setIsDashboardSlider(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = useAxiosError();
|
const { mutate: lockDashboard } = useMutation(locked, {
|
||||||
|
onSuccess: (_, props) => {
|
||||||
const { mutate: lockDashboard } = useMutation(lockDashboardApi, {
|
|
||||||
onSuccess: () => {
|
|
||||||
setIsDashboardSlider(false);
|
setIsDashboardSlider(false);
|
||||||
setIsDashboardLocked(true);
|
setIsDashboardLocked(props.lock);
|
||||||
},
|
},
|
||||||
onError: handleError,
|
onError: (error) => {
|
||||||
});
|
showErrorModal(error as APIError);
|
||||||
|
|
||||||
const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, {
|
|
||||||
onSuccess: () => {
|
|
||||||
setIsDashboardLocked(false);
|
|
||||||
},
|
},
|
||||||
onError: handleError,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
|
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
|
||||||
if (selectedDashboard) {
|
if (selectedDashboard) {
|
||||||
if (value) {
|
try {
|
||||||
lockDashboard(selectedDashboard);
|
await lockDashboard({
|
||||||
} else {
|
id: selectedDashboard.id,
|
||||||
unlockDashboard(selectedDashboard);
|
lock: value,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
export interface DashboardSortOrder {
|
export interface DashboardSortOrder {
|
||||||
@ -19,7 +20,7 @@ export interface IDashboardContext {
|
|||||||
isDashboardLocked: boolean;
|
isDashboardLocked: boolean;
|
||||||
handleToggleDashboardSlider: (value: boolean) => void;
|
handleToggleDashboardSlider: (value: boolean) => void;
|
||||||
handleDashboardLockToggle: (value: boolean) => void;
|
handleDashboardLockToggle: (value: boolean) => void;
|
||||||
dashboardResponse: UseQueryResult<Dashboard, unknown>;
|
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
||||||
selectedDashboard: Dashboard | undefined;
|
selectedDashboard: Dashboard | undefined;
|
||||||
dashboardId: string;
|
dashboardId: string;
|
||||||
layouts: Layout[];
|
layouts: Layout[];
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Dashboard, DashboardData } from './getAll';
|
import { Dashboard } from './getAll';
|
||||||
|
|
||||||
export type Props =
|
export type Props = {
|
||||||
| {
|
title: Dashboard['data']['title'];
|
||||||
title: Dashboard['data']['title'];
|
uploadedGrafana: boolean;
|
||||||
uploadedGrafana: boolean;
|
version?: string;
|
||||||
version?: string;
|
};
|
||||||
}
|
|
||||||
| { DashboardData: DashboardData; uploadedGrafana: boolean };
|
|
||||||
|
|
||||||
export type PayloadProps = Dashboard;
|
export interface PayloadProps {
|
||||||
|
data: Dashboard;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Dashboard } from './getAll';
|
import { Dashboard } from './getAll';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
uuid: Dashboard['uuid'];
|
id: Dashboard['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PayloadProps {
|
export interface PayloadProps {
|
||||||
status: 'success';
|
status: string;
|
||||||
|
data: null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Dashboard } from './getAll';
|
import { Dashboard } from './getAll';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
uuid: Dashboard['uuid'];
|
id: Dashboard['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayloadProps = Dashboard;
|
export interface PayloadProps {
|
||||||
|
data: Dashboard;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
@ -9,8 +9,6 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { IField } from '../logs/fields';
|
import { IField } from '../logs/fields';
|
||||||
import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
export type PayloadProps = Dashboard[];
|
|
||||||
|
|
||||||
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
|
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
|
||||||
export type TVariableQueryType = typeof VariableQueryTypeArr[number];
|
export type TVariableQueryType = typeof VariableQueryTypeArr[number];
|
||||||
|
|
||||||
@ -50,14 +48,18 @@ export interface IDashboardVariable {
|
|||||||
change?: boolean;
|
change?: boolean;
|
||||||
}
|
}
|
||||||
export interface Dashboard {
|
export interface Dashboard {
|
||||||
id: number;
|
id: string;
|
||||||
uuid: string;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
data: DashboardData;
|
data: DashboardData;
|
||||||
isLocked?: boolean;
|
locked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadProps {
|
||||||
|
data: Dashboard[];
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardTemplate {
|
export interface DashboardTemplate {
|
||||||
@ -69,7 +71,7 @@ export interface DashboardTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardData {
|
export interface DashboardData {
|
||||||
uuid?: string;
|
// uuid?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
name?: string;
|
name?: string;
|
||||||
|
11
frontend/src/types/api/dashboard/lockUnlock.ts
Normal file
11
frontend/src/types/api/dashboard/lockUnlock.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Dashboard } from './getAll';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
id: Dashboard['id'];
|
||||||
|
lock: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PayloadProps {
|
||||||
|
data: null;
|
||||||
|
status: string;
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
import { Dashboard, DashboardData } from './getAll';
|
import { Dashboard, DashboardData } from './getAll';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
uuid: Dashboard['uuid'];
|
id: Dashboard['id'];
|
||||||
data: DashboardData;
|
data: DashboardData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PayloadProps = Dashboard;
|
export interface PayloadProps {
|
||||||
|
data: Dashboard;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ export enum LicenseEvent {
|
|||||||
export enum LicenseStatus {
|
export enum LicenseStatus {
|
||||||
SUSPENDED = 'SUSPENDED',
|
SUSPENDED = 'SUSPENDED',
|
||||||
VALID = 'VALID',
|
VALID = 'VALID',
|
||||||
|
INVALID = 'INVALID',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LicenseState {
|
export enum LicenseState {
|
||||||
|
2
go.mod
2
go.mod
@ -26,7 +26,6 @@ require (
|
|||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/gosimple/slug v1.10.0
|
|
||||||
github.com/huandu/go-sqlbuilder v1.35.0
|
github.com/huandu/go-sqlbuilder v1.35.0
|
||||||
github.com/jackc/pgx/v5 v5.7.2
|
github.com/jackc/pgx/v5 v5.7.2
|
||||||
github.com/jmoiron/sqlx v1.3.4
|
github.com/jmoiron/sqlx v1.3.4
|
||||||
@ -138,7 +137,6 @@ require (
|
|||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
||||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||||
github.com/gosimple/unidecode v1.0.0 // indirect
|
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -450,10 +450,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gosimple/slug v1.10.0 h1:3XbiQua1IpCdrvuntWvGBxVm+K99wCSxJjlxkP49GGQ=
|
|
||||||
github.com/gosimple/slug v1.10.0/go.mod h1:MICb3w495l9KNdZm+Xn5b6T2Hn831f9DMxiJ1r+bAjw=
|
|
||||||
github.com/gosimple/unidecode v1.0.0 h1:kPdvM+qy0tnk4/BrnkrbdJ82xe88xn7c9hcaipDz4dQ=
|
|
||||||
github.com/gosimple/unidecode v1.0.0/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
@ -67,23 +67,6 @@ func (store *config) Set(ctx context.Context, config *alertmanagertypes.Config,
|
|||||||
}, opts...)
|
}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *config) ListOrgs(ctx context.Context) ([]string, error) {
|
|
||||||
var orgIDs []string
|
|
||||||
|
|
||||||
err := store.
|
|
||||||
sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewSelect().
|
|
||||||
Table("organizations").
|
|
||||||
ColumnExpr("id").
|
|
||||||
Scan(ctx, &orgIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return orgIDs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertypes.Channel, opts ...alertmanagertypes.StoreOption) error {
|
func (store *config) CreateChannel(ctx context.Context, channel *alertmanagertypes.Channel, opts ...alertmanagertypes.StoreOption) error {
|
||||||
return store.wrap(ctx, func(ctx context.Context) error {
|
return store.wrap(ctx, func(ctx context.Context) error {
|
||||||
if _, err := store.
|
if _, err := store.
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerbatcher"
|
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerbatcher"
|
||||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
@ -57,16 +58,17 @@ type provider struct {
|
|||||||
configStore alertmanagertypes.ConfigStore
|
configStore alertmanagertypes.ConfigStore
|
||||||
batcher *alertmanagerbatcher.Batcher
|
batcher *alertmanagerbatcher.Batcher
|
||||||
url *url.URL
|
url *url.URL
|
||||||
|
orgGetter organization.Getter
|
||||||
orgID string
|
orgID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||||
return factory.NewProviderFactory(factory.MustNewName("legacy"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
|
return factory.NewProviderFactory(factory.MustNewName("legacy"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
|
||||||
return New(ctx, settings, config, sqlstore)
|
return New(ctx, settings, config, sqlstore, orgGetter)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) {
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) (*provider, error) {
|
||||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/legacyalertmanager")
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/legacyalertmanager")
|
||||||
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
||||||
|
|
||||||
@ -92,7 +94,7 @@ func (provider *provider) Start(ctx context.Context) error {
|
|||||||
// For the first time, we need to get the orgID from the config store.
|
// For the first time, we need to get the orgID from the config store.
|
||||||
// Since this is the legacy alertmanager, we get the first org from the store.
|
// Since this is the legacy alertmanager, we get the first org from the store.
|
||||||
if provider.orgID == "" {
|
if provider.orgID == "" {
|
||||||
orgIDs, err := provider.configStore.ListOrgs(ctx)
|
orgIDs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err)
|
provider.settings.Logger().ErrorContext(ctx, "failed to send alerts to alertmanager", "error", err)
|
||||||
continue
|
continue
|
||||||
@ -103,7 +105,7 @@ func (provider *provider) Start(ctx context.Context) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.orgID = orgIDs[0]
|
provider.orgID = orgIDs[0].ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := provider.putAlerts(ctx, provider.orgID, alerts); err != nil {
|
if err := provider.putAlerts(ctx, provider.orgID, alerts); err != nil {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
|
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,6 +21,9 @@ type Service struct {
|
|||||||
// configStore is the config store for the alertmanager service
|
// configStore is the config store for the alertmanager service
|
||||||
configStore alertmanagertypes.ConfigStore
|
configStore alertmanagertypes.ConfigStore
|
||||||
|
|
||||||
|
// organization is the organization module for the alertmanager service
|
||||||
|
orgGetter organization.Getter
|
||||||
|
|
||||||
// settings is the settings for the alertmanager service
|
// settings is the settings for the alertmanager service
|
||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
|
|
||||||
@ -30,11 +34,19 @@ type Service struct {
|
|||||||
serversMtx sync.RWMutex
|
serversMtx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, settings factory.ScopedProviderSettings, config alertmanagerserver.Config, stateStore alertmanagertypes.StateStore, configStore alertmanagertypes.ConfigStore) *Service {
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
settings factory.ScopedProviderSettings,
|
||||||
|
config alertmanagerserver.Config,
|
||||||
|
stateStore alertmanagertypes.StateStore,
|
||||||
|
configStore alertmanagertypes.ConfigStore,
|
||||||
|
orgGetter organization.Getter,
|
||||||
|
) *Service {
|
||||||
service := &Service{
|
service := &Service{
|
||||||
config: config,
|
config: config,
|
||||||
stateStore: stateStore,
|
stateStore: stateStore,
|
||||||
configStore: configStore,
|
configStore: configStore,
|
||||||
|
orgGetter: orgGetter,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
servers: make(map[string]*alertmanagerserver.Server),
|
servers: make(map[string]*alertmanagerserver.Server),
|
||||||
serversMtx: sync.RWMutex{},
|
serversMtx: sync.RWMutex{},
|
||||||
@ -44,38 +56,38 @@ func New(ctx context.Context, settings factory.ScopedProviderSettings, config al
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) SyncServers(ctx context.Context) error {
|
func (service *Service) SyncServers(ctx context.Context) error {
|
||||||
orgIDs, err := service.configStore.ListOrgs(ctx)
|
orgs, err := service.orgGetter.ListByOwnedKeyRange(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
service.serversMtx.Lock()
|
service.serversMtx.Lock()
|
||||||
for _, orgID := range orgIDs {
|
for _, org := range orgs {
|
||||||
config, err := service.getConfig(ctx, orgID)
|
config, err := service.getConfig(ctx, org.ID.StringValue())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", orgID, "error", err)
|
service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", org.ID.StringValue(), "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the server is not present, create it and sync the config
|
// If the server is not present, create it and sync the config
|
||||||
if _, ok := service.servers[orgID]; !ok {
|
if _, ok := service.servers[org.ID.StringValue()]; !ok {
|
||||||
server, err := service.newServer(ctx, orgID)
|
server, err := service.newServer(ctx, org.ID.StringValue())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.settings.Logger().ErrorContext(ctx, "failed to create alertmanager server", "org_id", orgID, "error", err)
|
service.settings.Logger().ErrorContext(ctx, "failed to create alertmanager server", "org_id", org.ID.StringValue(), "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
service.servers[orgID] = server
|
service.servers[org.ID.StringValue()] = server
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.servers[orgID].Hash() == config.StoreableConfig().Hash {
|
if service.servers[org.ID.StringValue()].Hash() == config.StoreableConfig().Hash {
|
||||||
service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", "org_id", orgID, "hash", config.StoreableConfig().Hash)
|
service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", "org_id", org.ID.StringValue(), "hash", config.StoreableConfig().Hash)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.servers[orgID].SetConfig(ctx, config)
|
err = service.servers[org.ID.StringValue()].SetConfig(ctx, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
service.settings.Logger().ErrorContext(ctx, "failed to set config for alertmanager server", "org_id", orgID, "error", err)
|
service.settings.Logger().ErrorContext(ctx, "failed to set config for alertmanager server", "org_id", org.ID.StringValue(), "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerstore/sqlalertmanagerstore"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
@ -22,13 +23,13 @@ type provider struct {
|
|||||||
stopC chan struct{}
|
stopC chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
func NewFactory(sqlstore sqlstore.SQLStore, orgGetter organization.Getter) factory.ProviderFactory[alertmanager.Alertmanager, alertmanager.Config] {
|
||||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
|
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config alertmanager.Config) (alertmanager.Alertmanager, error) {
|
||||||
return New(ctx, settings, config, sqlstore)
|
return New(ctx, settings, config, sqlstore, orgGetter)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore) (*provider, error) {
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config alertmanager.Config, sqlstore sqlstore.SQLStore, orgGetter organization.Getter) (*provider, error) {
|
||||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager")
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager")
|
||||||
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
configStore := sqlalertmanagerstore.NewConfigStore(sqlstore)
|
||||||
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
|
stateStore := sqlalertmanagerstore.NewStateStore(sqlstore)
|
||||||
@ -40,6 +41,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
config.Signoz.Config,
|
config.Signoz.Config,
|
||||||
stateStore,
|
stateStore,
|
||||||
configStore,
|
configStore,
|
||||||
|
orgGetter,
|
||||||
),
|
),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
config: config,
|
config: config,
|
||||||
|
@ -5,9 +5,15 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiKeyCrossOrgMessage string = "::API-KEY-CROSS-ORG::"
|
||||||
)
|
)
|
||||||
|
|
||||||
type APIKey struct {
|
type APIKey struct {
|
||||||
@ -15,10 +21,11 @@ type APIKey struct {
|
|||||||
uuid *authtypes.UUID
|
uuid *authtypes.UUID
|
||||||
headers []string
|
headers []string
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
sharder sharder.Sharder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger) *APIKey {
|
func NewAPIKey(store sqlstore.SQLStore, headers []string, logger *slog.Logger, sharder sharder.Sharder) *APIKey {
|
||||||
return &APIKey{store: store, uuid: authtypes.NewUUID(), headers: headers, logger: logger}
|
return &APIKey{store: store, uuid: authtypes.NewUUID(), headers: headers, logger: logger, sharder: sharder}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *APIKey) Wrap(next http.Handler) http.Handler {
|
func (a *APIKey) Wrap(next http.Handler) http.Handler {
|
||||||
@ -36,13 +43,20 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
|
|||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKeyToken, ok := authtypes.UUIDFromContext(ctx)
|
apiKeyToken, ok := authtypes.UUIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.store.BunDB().NewSelect().Model(&apiKey).Where("token = ?", apiKeyToken).Scan(r.Context())
|
err = a.
|
||||||
|
store.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&apiKey).
|
||||||
|
Where("token = ?", apiKeyToken).
|
||||||
|
Scan(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
@ -71,6 +85,18 @@ func (a *APIKey) Wrap(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
ctx = authtypes.NewContextWithClaims(ctx, jwt)
|
ctx = authtypes.NewContextWithClaims(ctx, jwt)
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.sharder.IsMyOwnedKey(r.Context(), types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil {
|
||||||
|
a.logger.ErrorContext(r.Context(), apiKeyCrossOrgMessage, "claims", claims, "error", err)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
authCrossOrgMessage string = "::AUTH-CROSS-ORG::"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
jwt *authtypes.JWT
|
jwt *authtypes.JWT
|
||||||
headers []string
|
headers []string
|
||||||
|
sharder sharder.Sharder
|
||||||
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuth(jwt *authtypes.JWT, headers []string) *Auth {
|
func NewAuth(jwt *authtypes.JWT, headers []string, sharder sharder.Sharder, logger *slog.Logger) *Auth {
|
||||||
return &Auth{jwt: jwt, headers: headers}
|
return &Auth{jwt: jwt, headers: headers, sharder: sharder, logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) Wrap(next http.Handler) http.Handler {
|
func (a *Auth) Wrap(next http.Handler) http.Handler {
|
||||||
@ -28,6 +38,18 @@ func (a *Auth) Wrap(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.sharder.IsMyOwnedKey(r.Context(), types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil {
|
||||||
|
a.logger.ErrorContext(r.Context(), authCrossOrgMessage, "claims", claims, "error", err)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
@ -4,13 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types/apdextypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module interface {
|
type Module interface {
|
||||||
Get(context.Context, string, []string) ([]*types.ApdexSettings, error)
|
Get(context.Context, string, []string) ([]*apdextypes.Settings, error)
|
||||||
|
|
||||||
Set(context.Context, string, *types.ApdexSettings) error
|
Set(context.Context, string, *apdextypes.Settings) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/apdex"
|
"github.com/SigNoz/signoz/pkg/modules/apdex"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types/apdextypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ func (handler *handler) Set(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var apdexSettings types.ApdexSettings
|
var apdexSettings apdextypes.Settings
|
||||||
if err := json.NewDecoder(req.Body).Decode(&apdexSettings); err != nil {
|
if err := json.NewDecoder(req.Body).Decode(&apdexSettings); err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
return
|
return
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/apdex"
|
"github.com/SigNoz/signoz/pkg/modules/apdex"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types/apdextypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
@ -25,8 +25,8 @@ func NewModule(sqlstore sqlstore.SQLStore) apdex.Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (module *module) Get(ctx context.Context, orgID string, services []string) ([]*types.ApdexSettings, error) {
|
func (module *module) Get(ctx context.Context, orgID string, services []string) ([]*apdextypes.Settings, error) {
|
||||||
var apdexSettings []*types.ApdexSettings
|
var apdexSettings []*apdextypes.Settings
|
||||||
|
|
||||||
err := module.
|
err := module.
|
||||||
sqlstore.
|
sqlstore.
|
||||||
@ -51,7 +51,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
apdexSettings = append(apdexSettings, &types.ApdexSettings{
|
apdexSettings = append(apdexSettings, &apdextypes.Settings{
|
||||||
ServiceName: service,
|
ServiceName: service,
|
||||||
Threshold: defaultApdexThreshold,
|
Threshold: defaultApdexThreshold,
|
||||||
})
|
})
|
||||||
@ -61,7 +61,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
|
|||||||
return apdexSettings, nil
|
return apdexSettings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (module *module) Set(ctx context.Context, orgID string, apdexSettings *types.ApdexSettings) error {
|
func (module *module) Set(ctx context.Context, orgID string, apdexSettings *apdextypes.Settings) error {
|
||||||
apdexSettings.OrgID = orgID
|
apdexSettings.OrgID = orgID
|
||||||
apdexSettings.Identifiable.ID = valuer.GenerateUUID()
|
apdexSettings.Identifiable.ID = valuer.GenerateUUID()
|
||||||
|
|
||||||
|
@ -4,25 +4,32 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module interface {
|
type Module interface {
|
||||||
Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error)
|
Create(ctx context.Context, orgID valuer.UUID, createdBy string, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error)
|
||||||
|
|
||||||
List(ctx context.Context, orgID string) ([]*types.Dashboard, error)
|
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error)
|
||||||
|
|
||||||
Delete(ctx context.Context, orgID, uuid string) error
|
List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||||
|
|
||||||
Get(ctx context.Context, orgID, uuid string) (*types.Dashboard, error)
|
Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error)
|
||||||
|
|
||||||
GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error)
|
LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error
|
||||||
|
|
||||||
Update(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, error)
|
Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||||
|
|
||||||
LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error
|
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
|
Create(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
Update(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
LockUnlock(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
Delete(http.ResponseWriter, *http.Request)
|
Delete(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,16 @@ package impldashboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,8 +23,8 @@ func NewHandler(module dashboard.Module) dashboard.Handler {
|
|||||||
return &handler{module: module}
|
return &handler{module: module}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) {
|
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
@ -29,13 +33,151 @@ func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid := mux.Vars(req)["uuid"]
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.module.Delete(ctx, claims.OrgID, uuid)
|
req := dashboardtypes.PostableDashboard{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusCreated, gettableDashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dashboardID, err := valuer.NewUUID(id)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := dashboardtypes.UpdatableDashboard{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard, err := handler.module.Update(ctx, orgID, dashboardID, claims.Email, req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusOK, dashboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dashboardID, err := valuer.NewUUID(id)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(dashboardtypes.LockUnlockDashboard)
|
||||||
|
err = json.NewDecoder(r.Body).Decode(req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, *req.Locked)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
render.Success(rw, http.StatusOK, nil)
|
render.Success(rw, http.StatusOK, nil)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := mux.Vars(r)["id"]
|
||||||
|
if id == "" {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dashboardID, err := valuer.NewUUID(id)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = handler.module.Delete(ctx, orgID, dashboardID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusNoContent, nil)
|
||||||
}
|
}
|
||||||
|
@ -2,164 +2,40 @@ package impldashboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
"github.com/google/uuid"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type module struct {
|
type module struct {
|
||||||
sqlstore sqlstore.SQLStore
|
store dashboardtypes.Store
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModule(sqlstore sqlstore.SQLStore) dashboard.Module {
|
func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) dashboard.Module {
|
||||||
|
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/impldashboard")
|
||||||
return &module{
|
return &module{
|
||||||
sqlstore: sqlstore,
|
store: NewStore(sqlstore),
|
||||||
|
settings: scopedProviderSettings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDashboard creates a new dashboard
|
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||||
func (module *module) Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) {
|
dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
|
||||||
dash := &types.Dashboard{
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
dash.OrgID = orgID
|
|
||||||
dash.CreatedAt = time.Now()
|
|
||||||
dash.CreatedBy = email
|
|
||||||
dash.UpdatedAt = time.Now()
|
|
||||||
dash.UpdatedBy = email
|
|
||||||
dash.UpdateSlug()
|
|
||||||
dash.UUID = uuid.New().String()
|
|
||||||
if data["uuid"] != nil {
|
|
||||||
dash.UUID = data["uuid"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := module.
|
|
||||||
sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewInsert().
|
|
||||||
Model(dash).
|
|
||||||
Returning("id").
|
|
||||||
Scan(ctx, &dash.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, module.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard with uuid %s already exists", dash.UUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) List(ctx context.Context, orgID string) ([]*types.Dashboard, error) {
|
|
||||||
dashboards := []*types.Dashboard{}
|
|
||||||
|
|
||||||
err := module.
|
|
||||||
sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewSelect().
|
|
||||||
Model(&dashboards).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return dashboards, nil
|
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) Delete(ctx context.Context, orgID, uuid string) error {
|
|
||||||
dashboard, err := module.Get(ctx, orgID, uuid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
|
||||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be able to delete it")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := module.
|
|
||||||
sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewDelete().
|
|
||||||
Model(&types.Dashboard{}).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("uuid = ?", uuid).
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
affectedRows, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if affectedRows == 0 {
|
|
||||||
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no dashboard found with uuid: %s", uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) Get(ctx context.Context, orgID, uuid string) (*types.Dashboard, error) {
|
|
||||||
dashboard := types.Dashboard{}
|
|
||||||
err := module.
|
|
||||||
sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewSelect().
|
|
||||||
Model(&dashboard).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("uuid = ?", uuid).
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, module.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with uuid %s not found", uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dashboard, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, error) {
|
|
||||||
mapData, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = module.store.Create(ctx, storableDashboard)
|
||||||
dashboard, err := module.Get(ctx, orgID, uuid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if dashboard.Locked != nil && *dashboard.Locked == 1 {
|
|
||||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be able to edit it")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the total count of panels has reduced by more than 1,
|
|
||||||
// return error
|
|
||||||
existingIds := getWidgetIds(dashboard.Data)
|
|
||||||
newIds := getWidgetIds(data)
|
|
||||||
|
|
||||||
differenceIds := getIdDifference(existingIds, newIds)
|
|
||||||
|
|
||||||
if len(differenceIds) > 1 {
|
|
||||||
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "deleting more than one panel is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboard.UpdatedAt = time.Now()
|
|
||||||
dashboard.UpdatedBy = userEmail
|
|
||||||
dashboard.Data = data
|
|
||||||
|
|
||||||
_, err = module.sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewUpdate().
|
|
||||||
Model(dashboard).
|
|
||||||
Set("updated_at = ?", dashboard.UpdatedAt).
|
|
||||||
Set("updated_by = ?", userEmail).
|
|
||||||
Set("data = ?", mapData).
|
|
||||||
Where("uuid = ?", dashboard.UUID).Exec(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -167,28 +43,73 @@ func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string,
|
|||||||
return dashboard, nil
|
return dashboard, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error {
|
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
||||||
dashboard, err := module.Get(ctx, orgID, uuid)
|
storableDashboard, err := module.store.Get(ctx, orgID, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard, err := dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dashboard, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
||||||
|
storableDashboards, err := module.store.List(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboards, err := dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboards, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error) {
|
||||||
|
dashboard, err := module.Get(ctx, orgID, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dashboard.Update(updatableDashboard, updatedBy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = module.store.Update(ctx, orgID, storableDashboard)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboard, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error {
|
||||||
|
dashboard, err := module.Get(ctx, orgID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var lockValue int
|
err = dashboard.LockUnlock(ctx, lock, updatedBy)
|
||||||
if lock {
|
if err != nil {
|
||||||
lockValue = 1
|
return err
|
||||||
} else {
|
}
|
||||||
lockValue = 0
|
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = module.
|
err = module.store.Update(ctx, orgID, storableDashboard)
|
||||||
sqlstore.
|
|
||||||
BunDB().
|
|
||||||
NewUpdate().
|
|
||||||
Model(dashboard).
|
|
||||||
Set("locked = ?", lockValue).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("uuid = ?", uuid).
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -196,15 +117,21 @@ func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock b
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (module *module) GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) {
|
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||||
dashboards := []types.Dashboard{}
|
dashboard, err := module.Get(ctx, orgID, id)
|
||||||
err := module.
|
if err != nil {
|
||||||
sqlstore.
|
return err
|
||||||
BunDB().
|
}
|
||||||
NewSelect().
|
|
||||||
Model(&dashboards).
|
if dashboard.Locked {
|
||||||
Where("org_id = ?", orgID).
|
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
|
||||||
Scan(ctx)
|
}
|
||||||
|
|
||||||
|
return module.store.Delete(ctx, orgID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error) {
|
||||||
|
dashboards, err := module.List(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -266,7 +193,7 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
|
|||||||
for _, metricName := range metricNames {
|
for _, metricName := range metricNames {
|
||||||
if strings.TrimSpace(key) == metricName {
|
if strings.TrimSpace(key) == metricName {
|
||||||
result[metricName] = append(result[metricName], map[string]string{
|
result[metricName] = append(result[metricName], map[string]string{
|
||||||
"dashboard_id": dashboard.UUID,
|
"dashboard_id": dashboard.ID,
|
||||||
"widget_name": widgetTitle,
|
"widget_name": widgetTitle,
|
||||||
"widget_id": widgetID,
|
"widget_id": widgetID,
|
||||||
"dashboard_name": dashTitle,
|
"dashboard_name": dashTitle,
|
||||||
@ -280,52 +207,3 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWidgetIds(data map[string]interface{}) []string {
|
|
||||||
widgetIds := []string{}
|
|
||||||
if data != nil && data["widgets"] != nil {
|
|
||||||
widgets, ok := data["widgets"]
|
|
||||||
if ok {
|
|
||||||
data, ok := widgets.([]interface{})
|
|
||||||
if ok {
|
|
||||||
for _, widget := range data {
|
|
||||||
sData, ok := widget.(map[string]interface{})
|
|
||||||
if ok && sData["query"] != nil && sData["id"] != nil {
|
|
||||||
id, ok := sData["id"].(string)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
widgetIds = append(widgetIds, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return widgetIds
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIdDifference(existingIds []string, newIds []string) []string {
|
|
||||||
// Convert newIds array to a map for faster lookups
|
|
||||||
newIdsMap := make(map[string]bool)
|
|
||||||
for _, id := range newIds {
|
|
||||||
newIdsMap[id] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize a map to keep track of elements in the difference array
|
|
||||||
differenceMap := make(map[string]bool)
|
|
||||||
|
|
||||||
// Initialize the difference array
|
|
||||||
difference := []string{}
|
|
||||||
|
|
||||||
// Iterate through existingIds
|
|
||||||
for _, id := range existingIds {
|
|
||||||
// If the id is not found in newIds, and it's not already in the difference array
|
|
||||||
if _, found := newIdsMap[id]; !found && !differenceMap[id] {
|
|
||||||
difference = append(difference, id)
|
|
||||||
differenceMap[id] = true // Mark the id as seen in the difference array
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return difference
|
|
||||||
}
|
|
||||||
|
99
pkg/modules/dashboard/impldashboard/store.go
Normal file
99
pkg/modules/dashboard/impldashboard/store.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package impldashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(sqlstore sqlstore.SQLStore) dashboardtypes.Store {
|
||||||
|
return &store{sqlstore: sqlstore}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Create(ctx context.Context, storabledashboard *dashboardtypes.StorableDashboard) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewInsert().
|
||||||
|
Model(storabledashboard).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard with id %s already exists", storabledashboard.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.StorableDashboard, error) {
|
||||||
|
storableDashboard := new(dashboardtypes.StorableDashboard)
|
||||||
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(storableDashboard).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storableDashboard, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.StorableDashboard, error) {
|
||||||
|
storableDashboards := make([]*dashboardtypes.StorableDashboard, 0)
|
||||||
|
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&storableDashboards).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no dashboards found in orgID %s", orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storableDashboards, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashboard *dashboardtypes.StorableDashboard) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewUpdate().
|
||||||
|
Model(storableDashboard).
|
||||||
|
WherePK().
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "dashboard with id %s doesn't exist", storableDashboard.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewDelete().
|
||||||
|
Model(new(dashboardtypes.StorableDashboard)).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
36
pkg/modules/organization/implorganization/getter.go
Normal file
36
pkg/modules/organization/implorganization/getter.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package implorganization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getter struct {
|
||||||
|
store types.OrganizationStore
|
||||||
|
sharder sharder.Sharder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetter(store types.OrganizationStore, sharder sharder.Sharder) organization.Getter {
|
||||||
|
return &getter{store: store, sharder: sharder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Organization, error) {
|
||||||
|
return module.store.Get(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *getter) List(ctx context.Context) ([]*types.Organization, error) {
|
||||||
|
return module.store.GetAll(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *getter) ListByOwnedKeyRange(ctx context.Context) ([]*types.Organization, error) {
|
||||||
|
start, end, err := module.sharder.GetMyOwnedKeyRange(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return module.store.ListByKeyRange(ctx, start, end)
|
||||||
|
}
|
@ -15,11 +15,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
module organization.Module
|
orgGetter organization.Getter
|
||||||
|
orgSetter organization.Setter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(module organization.Module) organization.Handler {
|
func NewHandler(orgGetter organization.Getter, orgSetter organization.Setter) organization.Handler {
|
||||||
return &handler{module: module}
|
return &handler{orgGetter: orgGetter, orgSetter: orgSetter}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||||
@ -38,7 +39,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
organization, err := handler.module.Get(ctx, orgID)
|
organization, err := handler.orgGetter.Get(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
return
|
return
|
||||||
@ -67,10 +68,11 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.NewDecoder(r.Body).Decode(&req)
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.ID = orgID
|
req.ID = orgID
|
||||||
err = handler.module.Update(ctx, req)
|
err = handler.orgSetter.Update(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(rw, err)
|
render.Error(rw, err)
|
||||||
return
|
return
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
package implorganization
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type module struct {
|
|
||||||
store types.OrganizationStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewModule(organizationStore types.OrganizationStore) organization.Module {
|
|
||||||
return &module{store: organizationStore}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) Create(ctx context.Context, organization *types.Organization) error {
|
|
||||||
return module.store.Create(ctx, organization)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) Get(ctx context.Context, id valuer.UUID) (*types.Organization, error) {
|
|
||||||
return module.store.Get(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) GetAll(ctx context.Context) ([]*types.Organization, error) {
|
|
||||||
return module.store.GetAll(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (module *module) Update(ctx context.Context, updatedOrganization *types.Organization) error {
|
|
||||||
return module.store.Update(ctx, updatedOrganization)
|
|
||||||
}
|
|
40
pkg/modules/organization/implorganization/setter.go
Normal file
40
pkg/modules/organization/implorganization/setter.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package implorganization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type setter struct {
|
||||||
|
store types.OrganizationStore
|
||||||
|
alertmanager alertmanager.Alertmanager
|
||||||
|
quickfilter quickfilter.Module
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
|
||||||
|
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *setter) Create(ctx context.Context, organization *types.Organization) error {
|
||||||
|
if err := module.store.Create(ctx, organization); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := module.alertmanager.SetDefaultConfig(ctx, organization.ID.StringValue()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := module.quickfilter.SetDefaultConfig(ctx, organization.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *setter) Update(ctx context.Context, updatedOrganization *types.Organization) error {
|
||||||
|
return module.store.Update(ctx, updatedOrganization)
|
||||||
|
}
|
@ -92,3 +92,20 @@ func (store *store) Delete(ctx context.Context, id valuer.UUID) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store *store) ListByKeyRange(ctx context.Context, start, end uint32) ([]*types.Organization, error) {
|
||||||
|
organizations := make([]*types.Organization, 0)
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&organizations).
|
||||||
|
Where("key >= ?", start).
|
||||||
|
Where("key <= ?", end).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizations, nil
|
||||||
|
}
|
||||||
|
@ -8,17 +8,22 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module interface {
|
type Getter interface {
|
||||||
// Create creates the given organization
|
|
||||||
Create(context.Context, *types.Organization) error
|
|
||||||
|
|
||||||
// Get gets the organization based on the given id
|
// Get gets the organization based on the given id
|
||||||
Get(context.Context, valuer.UUID) (*types.Organization, error)
|
Get(context.Context, valuer.UUID) (*types.Organization, error)
|
||||||
|
|
||||||
// GetAll gets all the organizations
|
// Lists all the organizations
|
||||||
GetAll(context.Context) ([]*types.Organization, error)
|
List(context.Context) ([]*types.Organization, error)
|
||||||
|
|
||||||
// Update updates the given organization based on id present
|
// ListByOwnedKeyRange gets all the organizations owned by the instance
|
||||||
|
ListByOwnedKeyRange(context.Context) ([]*types.Organization, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Setter interface {
|
||||||
|
// Create creates the given organization
|
||||||
|
Create(context.Context, *types.Organization) error
|
||||||
|
|
||||||
|
// Update updates the given organization
|
||||||
Update(context.Context, *types.Organization) error
|
Update(context.Context, *types.Organization) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
235
pkg/modules/tracefunnel/impltracefunnel/handler.go
Normal file
235
pkg/modules/tracefunnel/impltracefunnel/handler.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package impltracefunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
tf "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
module tracefunnel.Module
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(module tracefunnel.Module) tracefunnel.Handler {
|
||||||
|
return &handler{module: module}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var req tf.PostableFunnel
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to create funnel: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := tf.ConstructFunnelResponse(funnel, &claims)
|
||||||
|
render.Success(rw, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var req tf.PostableFunnel
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funnel, err := handler.module.Get(r.Context(), req.FunnelID, valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"funnel not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
steps, err := tf.ProcessFunnelSteps(req.Steps)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funnel.Steps = steps
|
||||||
|
funnel.UpdatedAt = updatedAt
|
||||||
|
funnel.UpdatedBy = claims.UserID
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
funnel.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
funnel.Description = req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to update funnel in database: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to get updated funnel: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
|
||||||
|
render.Success(rw, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var req tf.PostableFunnel
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
funnelID := vars["funnel_id"]
|
||||||
|
|
||||||
|
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"funnel not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funnel.UpdatedAt = updatedAt
|
||||||
|
funnel.UpdatedBy = claims.UserID
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
funnel.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
funnel.Description = req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to update funnel in database: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to get updated funnel: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
|
||||||
|
render.Success(rw, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funnels, err := handler.module.List(r.Context(), valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to list funnels: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var response []tf.GettableFunnel
|
||||||
|
for _, f := range funnels {
|
||||||
|
response = append(response, tf.ConstructFunnelResponse(f, &claims))
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
funnelID := vars["funnel_id"]
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funnel, err := handler.module.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"funnel not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response := tf.ConstructFunnelResponse(funnel, &claims)
|
||||||
|
render.Success(rw, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
funnelID := vars["funnel_id"]
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler.module.Delete(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID)); err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"failed to delete funnel: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusOK, nil)
|
||||||
|
}
|
173
pkg/modules/tracefunnel/impltracefunnel/handler_test.go
Normal file
173
pkg/modules/tracefunnel/impltracefunnel/handler_test.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package impltracefunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockModule struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
|
args := m.Called(ctx, timestamp, name, userID, orgID)
|
||||||
|
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
|
args := m.Called(ctx, funnelID, orgID)
|
||||||
|
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
|
||||||
|
args := m.Called(ctx, funnel, userID)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||||
|
args := m.Called(ctx, orgID)
|
||||||
|
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||||
|
args := m.Called(ctx, funnelID, orgID)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID, orgID valuer.UUID) error {
|
||||||
|
args := m.Called(ctx, funnel, userID, orgID)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockModule) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||||
|
args := m.Called(ctx, funnelID, orgID)
|
||||||
|
return args.Get(0).(int64), args.Get(1).(int64), args.String(2), args.Error(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_List(t *testing.T) {
|
||||||
|
mockModule := new(MockModule)
|
||||||
|
handler := NewHandler(mockModule)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/list", nil)
|
||||||
|
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
claims := authtypes.Claims{
|
||||||
|
OrgID: orgID.String(),
|
||||||
|
}
|
||||||
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
funnel1ID := valuer.GenerateUUID()
|
||||||
|
funnel2ID := valuer.GenerateUUID()
|
||||||
|
expectedFunnels := []*traceFunnels.StorableFunnel{
|
||||||
|
{
|
||||||
|
Identifiable: types.Identifiable{
|
||||||
|
ID: funnel1ID,
|
||||||
|
},
|
||||||
|
Name: "funnel-1",
|
||||||
|
OrgID: orgID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Identifiable: types.Identifiable{
|
||||||
|
ID: funnel2ID,
|
||||||
|
},
|
||||||
|
Name: "funnel-2",
|
||||||
|
OrgID: orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockModule.On("List", req.Context(), orgID).Return(expectedFunnels, nil)
|
||||||
|
|
||||||
|
handler.List(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data []traceFunnels.GettableFunnel `json:"data"`
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "success", response.Status)
|
||||||
|
assert.Len(t, response.Data, 2)
|
||||||
|
assert.Equal(t, "funnel-1", response.Data[0].FunnelName)
|
||||||
|
assert.Equal(t, "funnel-2", response.Data[1].FunnelName)
|
||||||
|
|
||||||
|
mockModule.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Get(t *testing.T) {
|
||||||
|
mockModule := new(MockModule)
|
||||||
|
handler := NewHandler(mockModule)
|
||||||
|
|
||||||
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
||||||
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
|
||||||
|
OrgID: orgID.String(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||||
|
Identifiable: types.Identifiable{
|
||||||
|
ID: funnelID,
|
||||||
|
},
|
||||||
|
Name: "test-funnel",
|
||||||
|
OrgID: orgID,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockModule.On("Get", req.Context(), funnelID, orgID).Return(expectedFunnel, nil)
|
||||||
|
|
||||||
|
handler.Get(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data traceFunnels.GettableFunnel `json:"data"`
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "success", response.Status)
|
||||||
|
assert.Equal(t, "test-funnel", response.Data.FunnelName)
|
||||||
|
assert.Equal(t, expectedFunnel.OrgID.String(), response.Data.OrgID)
|
||||||
|
|
||||||
|
mockModule.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Delete(t *testing.T) {
|
||||||
|
mockModule := new(MockModule)
|
||||||
|
handler := NewHandler(mockModule)
|
||||||
|
|
||||||
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
||||||
|
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
|
||||||
|
OrgID: orgID.String(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
mockModule.On("Delete", req.Context(), funnelID, orgID).Return(nil)
|
||||||
|
|
||||||
|
handler.Delete(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
mockModule.AssertExpectations(t)
|
||||||
|
}
|
96
pkg/modules/tracefunnel/impltracefunnel/module.go
Normal file
96
pkg/modules/tracefunnel/impltracefunnel/module.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package impltracefunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type module struct {
|
||||||
|
store traceFunnels.FunnelStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModule(store traceFunnels.FunnelStore) tracefunnel.Module {
|
||||||
|
return &module{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (module *module) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
|
funnel := &traceFunnels.StorableFunnel{
|
||||||
|
Name: name,
|
||||||
|
OrgID: orgID,
|
||||||
|
}
|
||||||
|
funnel.CreatedAt = time.Unix(0, timestamp*1000000) // Convert to nanoseconds
|
||||||
|
funnel.CreatedBy = userID.String()
|
||||||
|
|
||||||
|
// Set up the user relationship
|
||||||
|
funnel.CreatedByUser = &types.User{
|
||||||
|
Identifiable: types.Identifiable{
|
||||||
|
ID: userID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if funnel.ID.IsZero() {
|
||||||
|
funnel.ID = valuer.GenerateUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
if funnel.CreatedAt.IsZero() {
|
||||||
|
funnel.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if funnel.UpdatedAt.IsZero() {
|
||||||
|
funnel.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set created_by if CreatedByUser is present
|
||||||
|
if funnel.CreatedByUser != nil {
|
||||||
|
funnel.CreatedBy = funnel.CreatedByUser.Identifiable.ID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := module.store.Create(ctx, funnel); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create funnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return funnel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a funnel by ID
|
||||||
|
func (module *module) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
|
return module.store.Get(ctx, funnelID, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates a funnel
|
||||||
|
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
|
||||||
|
funnel.UpdatedBy = userID.String()
|
||||||
|
return module.store.Update(ctx, funnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists all funnels for an organization
|
||||||
|
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||||
|
funnels, err := module.store.List(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list funnels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return funnels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a funnel
|
||||||
|
func (module *module) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||||
|
return module.store.Delete(ctx, funnelID, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFunnelMetadata gets metadata for a funnel
|
||||||
|
func (module *module) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||||
|
funnel, err := module.store.Get(ctx, funnelID, orgID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return funnel.CreatedAt.UnixNano() / 1000000, funnel.UpdatedAt.UnixNano() / 1000000, funnel.Description, nil
|
||||||
|
}
|
114
pkg/modules/tracefunnel/impltracefunnel/store.go
Normal file
114
pkg/modules/tracefunnel/impltracefunnel/store.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package impltracefunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(sqlstore sqlstore.SQLStore) traceFunnels.FunnelStore {
|
||||||
|
return &store{sqlstore: sqlstore}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||||
|
// Check if a funnel with the same name already exists in the organization
|
||||||
|
exists, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(new(traceFunnels.StorableFunnel)).
|
||||||
|
Where("name = ? AND org_id = ?", funnel.Name, funnel.OrgID.String()).
|
||||||
|
Exists(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to check for existing funnelr")
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return store.sqlstore.WrapAlreadyExistsErrf(nil, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewInsert().
|
||||||
|
Model(funnel).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create funnels")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a funnel by ID
|
||||||
|
func (store *store) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
|
funnel := &traceFunnels.StorableFunnel{}
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(funnel).
|
||||||
|
Relation("CreatedByUser").
|
||||||
|
Where("?TableAlias.id = ? AND ?TableAlias.org_id = ?", uuid.String(), orgID.String()).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get funnels")
|
||||||
|
}
|
||||||
|
return funnel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing funnel
|
||||||
|
func (store *store) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||||
|
funnel.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewUpdate().
|
||||||
|
Model(funnel).
|
||||||
|
WherePK().
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return store.sqlstore.WrapAlreadyExistsErrf(err, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves all funnels for a given organization
|
||||||
|
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||||
|
var funnels []*traceFunnels.StorableFunnel
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&funnels).
|
||||||
|
Relation("CreatedByUser").
|
||||||
|
Where("?TableAlias.org_id = ?", orgID.String()).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to list funnels")
|
||||||
|
}
|
||||||
|
return funnels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a funnel by ID
|
||||||
|
func (store *store) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewDelete().
|
||||||
|
Model(new(traceFunnels.StorableFunnel)).
|
||||||
|
Where("id = ? AND org_id = ?", funnelID.String(), orgID.String()).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete funnel")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
38
pkg/modules/tracefunnel/tracefunnel.go
Normal file
38
pkg/modules/tracefunnel/tracefunnel.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package tracefunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module defines the interface for trace funnel operations
|
||||||
|
type Module interface {
|
||||||
|
Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||||
|
|
||||||
|
Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||||
|
|
||||||
|
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error
|
||||||
|
|
||||||
|
List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error)
|
||||||
|
|
||||||
|
Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error
|
||||||
|
|
||||||
|
GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
New(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
UpdateSteps(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
UpdateFunnel(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
List(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
Get(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
Delete(http.ResponseWriter, *http.Request)
|
||||||
|
}
|
183
pkg/modules/tracefunnel/tracefunneltest/module_test.go
Normal file
183
pkg/modules/tracefunnel/tracefunneltest/module_test.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package tracefunneltest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockStore struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||||
|
args := m.Called(ctx, funnel)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||||
|
args := m.Called(ctx, uuid, orgID)
|
||||||
|
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockStore) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||||
|
args := m.Called(ctx, orgID)
|
||||||
|
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||||
|
args := m.Called(ctx, funnel)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
|
||||||
|
args := m.Called(ctx, uuid, orgID)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModule_Create(t *testing.T) {
|
||||||
|
mockStore := new(MockStore)
|
||||||
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
timestamp := time.Now().UnixMilli()
|
||||||
|
name := "test-funnel"
|
||||||
|
userID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
|
||||||
|
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
||||||
|
return f.Name == name &&
|
||||||
|
f.CreatedBy == userID.String() &&
|
||||||
|
f.OrgID == orgID &&
|
||||||
|
f.CreatedByUser != nil &&
|
||||||
|
f.CreatedByUser.ID == userID &&
|
||||||
|
f.CreatedAt.UnixNano()/1000000 == timestamp
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
|
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, funnel)
|
||||||
|
assert.Equal(t, name, funnel.Name)
|
||||||
|
assert.Equal(t, userID.String(), funnel.CreatedBy)
|
||||||
|
assert.Equal(t, orgID, funnel.OrgID)
|
||||||
|
assert.NotNil(t, funnel.CreatedByUser)
|
||||||
|
assert.Equal(t, userID, funnel.CreatedByUser.ID)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModule_Get(t *testing.T) {
|
||||||
|
mockStore := new(MockStore)
|
||||||
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||||
|
Name: "test-funnel",
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
|
||||||
|
|
||||||
|
funnel, err := module.Get(ctx, funnelID, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedFunnel, funnel)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModule_Update(t *testing.T) {
|
||||||
|
mockStore := new(MockStore)
|
||||||
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
userID := valuer.GenerateUUID()
|
||||||
|
funnel := &traceFunnels.StorableFunnel{
|
||||||
|
Name: "test-funnel",
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStore.On("Update", ctx, funnel).Return(nil)
|
||||||
|
|
||||||
|
err := module.Update(ctx, funnel, userID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, userID.String(), funnel.UpdatedBy)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModule_List(t *testing.T) {
|
||||||
|
mockStore := new(MockStore)
|
||||||
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
expectedFunnels := []*traceFunnels.StorableFunnel{
|
||||||
|
{
|
||||||
|
Name: "funnel-1",
|
||||||
|
OrgID: orgID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "funnel-2",
|
||||||
|
OrgID: orgID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStore.On("List", ctx, orgID).Return(expectedFunnels, nil)
|
||||||
|
|
||||||
|
funnels, err := module.List(ctx, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, funnels, 2)
|
||||||
|
assert.Equal(t, expectedFunnels, funnels)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModule_Delete(t *testing.T) {
|
||||||
|
mockStore := new(MockStore)
|
||||||
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
|
||||||
|
mockStore.On("Delete", ctx, funnelID, orgID).Return(nil)
|
||||||
|
|
||||||
|
err := module.Delete(ctx, funnelID, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModule_GetFunnelMetadata(t *testing.T) {
|
||||||
|
mockStore := new(MockStore)
|
||||||
|
module := impltracefunnel.NewModule(mockStore)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
funnelID := valuer.GenerateUUID()
|
||||||
|
orgID := valuer.GenerateUUID()
|
||||||
|
now := time.Now()
|
||||||
|
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||||
|
Description: "test description",
|
||||||
|
TimeAuditable: types.TimeAuditable{
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
|
||||||
|
|
||||||
|
createdAt, updatedAt, description, err := module.GetFunnelMetadata(ctx, funnelID, orgID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, now.UnixNano()/1000000, createdAt)
|
||||||
|
assert.Equal(t, now.UnixNano()/1000000, updatedAt)
|
||||||
|
assert.Equal(t, "test description", description)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
@ -11,8 +11,10 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/emailing"
|
"github.com/SigNoz/signoz/pkg/emailing"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
|
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
@ -22,20 +24,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
store types.UserStore
|
store types.UserStore
|
||||||
jwt *authtypes.JWT
|
jwt *authtypes.JWT
|
||||||
emailing emailing.Emailing
|
emailing emailing.Emailing
|
||||||
settings factory.ScopedProviderSettings
|
settings factory.ScopedProviderSettings
|
||||||
|
orgSetter organization.Setter
|
||||||
}
|
}
|
||||||
|
|
||||||
// This module is a WIP, don't take inspiration from this.
|
// This module is a WIP, don't take inspiration from this.
|
||||||
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter) user.Module {
|
||||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
||||||
return &Module{
|
return &Module{
|
||||||
store: store,
|
store: store,
|
||||||
jwt: jwt,
|
jwt: jwt,
|
||||||
emailing: emailing,
|
emailing: emailing,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
|
orgSetter: orgSetter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,3 +542,36 @@ func (m *Module) ListDomains(ctx context.Context, orgID valuer.UUID) ([]*types.G
|
|||||||
func (m *Module) UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) error {
|
func (m *Module) UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) error {
|
||||||
return m.store.UpdateDomain(ctx, domain)
|
return m.store.UpdateDomain(ctx, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Module) Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error) {
|
||||||
|
if req.Email == "" {
|
||||||
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "email is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Password == "" {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
organization := types.NewOrganization(req.OrgDisplayName)
|
||||||
|
err := m.orgSetter.Create(ctx, organization)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.InternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := types.NewUser(req.Name, req.Email, types.RoleAdmin.String(), organization.ID.StringValue())
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.InternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := types.NewFactorPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.InternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = m.CreateUserWithPassword(ctx, user, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.InternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
@ -62,6 +62,9 @@ type Module interface {
|
|||||||
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error)
|
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error)
|
||||||
RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error
|
RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error
|
||||||
GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error)
|
GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error)
|
||||||
|
|
||||||
|
// Register
|
||||||
|
Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin) (*types.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
|
@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
|
promValue "github.com/prometheus/prometheus/model/value"
|
||||||
"github.com/prometheus/prometheus/prompb"
|
"github.com/prometheus/prometheus/prompb"
|
||||||
"github.com/prometheus/prometheus/storage"
|
"github.com/prometheus/prometheus/storage"
|
||||||
"github.com/prometheus/prometheus/storage/remote"
|
"github.com/prometheus/prometheus/storage/remote"
|
||||||
@ -188,9 +190,10 @@ func (client *client) querySamples(ctx context.Context, start int64, end int64,
|
|||||||
var fingerprint, prevFingerprint uint64
|
var fingerprint, prevFingerprint uint64
|
||||||
var timestampMs int64
|
var timestampMs int64
|
||||||
var value float64
|
var value float64
|
||||||
|
var flags uint32
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
if err := rows.Scan(&metricName, &fingerprint, ×tampMs, &value); err != nil {
|
if err := rows.Scan(&metricName, &fingerprint, ×tampMs, &value, &flags); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +211,10 @@ func (client *client) querySamples(ctx context.Context, start int64, end int64,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flags&1 == 1 {
|
||||||
|
value = math.Float64frombits(promValue.StaleNaN)
|
||||||
|
}
|
||||||
|
|
||||||
// add samples to current time series
|
// add samples to current time series
|
||||||
ts.Samples = append(ts.Samples, prompb.Sample{
|
ts.Samples = append(ts.Samples, prompb.Sample{
|
||||||
Timestamp: timestampMs,
|
Timestamp: timestampMs,
|
||||||
|
@ -6267,9 +6267,6 @@ func (r *ClickHouseReader) GetUpdatedMetricsMetadata(ctx context.Context, orgID
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
cachedMetadata[metricName] = metadata
|
cachedMetadata[metricName] = metadata
|
||||||
} else {
|
} else {
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Error retrieving metrics metadata from cache", zap.String("metric_name", metricName), zap.Error(err))
|
|
||||||
}
|
|
||||||
missingMetrics = append(missingMetrics, metricName)
|
missingMetrics = append(missingMetrics, metricName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,9 +34,7 @@ type Controller struct {
|
|||||||
serviceConfigRepo ServiceConfigDatabase
|
serviceConfigRepo ServiceConfigDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(sqlStore sqlstore.SQLStore) (
|
func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
|
||||||
*Controller, error,
|
|
||||||
) {
|
|
||||||
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore)
|
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
|
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
|
||||||
@ -55,9 +55,7 @@ type ConnectedAccountsListResponse struct {
|
|||||||
Accounts []types.Account `json:"accounts"`
|
Accounts []types.Account `json:"accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ListConnectedAccounts(
|
func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
|
||||||
ctx context.Context, orgId string, cloudProvider string,
|
|
||||||
) (
|
|
||||||
*ConnectedAccountsListResponse, *model.ApiError,
|
*ConnectedAccountsListResponse, *model.ApiError,
|
||||||
) {
|
) {
|
||||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||||
@ -103,9 +101,7 @@ type GenerateConnectionUrlResponse struct {
|
|||||||
ConnectionUrl string `json:"connection_url"`
|
ConnectionUrl string `json:"connection_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GenerateConnectionUrl(
|
func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest) (*GenerateConnectionUrlResponse, *model.ApiError) {
|
||||||
ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest,
|
|
||||||
) (*GenerateConnectionUrlResponse, *model.ApiError) {
|
|
||||||
// Account connection with a simple connection URL may not be available for all providers.
|
// Account connection with a simple connection URL may not be available for all providers.
|
||||||
if cloudProvider != "aws" {
|
if cloudProvider != "aws" {
|
||||||
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
|
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
|
||||||
@ -154,9 +150,7 @@ type AccountStatusResponse struct {
|
|||||||
Status types.AccountStatus `json:"status"`
|
Status types.AccountStatus `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GetAccountStatus(
|
func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) (
|
||||||
ctx context.Context, orgId string, cloudProvider string, accountId string,
|
|
||||||
) (
|
|
||||||
*AccountStatusResponse, *model.ApiError,
|
*AccountStatusResponse, *model.ApiError,
|
||||||
) {
|
) {
|
||||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||||
@ -198,9 +192,7 @@ type IntegrationConfigForAgent struct {
|
|||||||
TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"`
|
TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) CheckInAsAgent(
|
func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest) (*AgentCheckInResponse, error) {
|
||||||
ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest,
|
|
||||||
) (*AgentCheckInResponse, error) {
|
|
||||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
@ -293,13 +285,7 @@ type UpdateAccountConfigRequest struct {
|
|||||||
Config types.AccountConfig `json:"config"`
|
Config types.AccountConfig `json:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) UpdateAccountConfig(
|
func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*types.Account, *model.ApiError) {
|
||||||
ctx context.Context,
|
|
||||||
orgId string,
|
|
||||||
cloudProvider string,
|
|
||||||
accountId string,
|
|
||||||
req UpdateAccountConfigRequest,
|
|
||||||
) (*types.Account, *model.ApiError) {
|
|
||||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
@ -316,9 +302,7 @@ func (c *Controller) UpdateAccountConfig(
|
|||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) DisconnectAccount(
|
func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*types.CloudIntegration, *model.ApiError) {
|
||||||
ctx context.Context, orgId string, cloudProvider string, accountId string,
|
|
||||||
) (*types.CloudIntegration, *model.ApiError) {
|
|
||||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
@ -520,10 +504,8 @@ func (c *Controller) UpdateServiceConfig(
|
|||||||
|
|
||||||
// All dashboards that are available based on cloud integrations configuration
|
// All dashboards that are available based on cloud integrations configuration
|
||||||
// across all cloud providers
|
// across all cloud providers
|
||||||
func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) (
|
func (c *Controller) AvailableDashboards(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
|
||||||
[]*types.Dashboard, *model.ApiError,
|
allDashboards := []*dashboardtypes.Dashboard{}
|
||||||
) {
|
|
||||||
allDashboards := []*types.Dashboard{}
|
|
||||||
|
|
||||||
for _, provider := range []string{"aws"} {
|
for _, provider := range []string{"aws"} {
|
||||||
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
|
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
|
||||||
@ -539,11 +521,8 @@ func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) (
|
|||||||
return allDashboards, nil
|
return allDashboards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) AvailableDashboardsForCloudProvider(
|
func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID valuer.UUID, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
|
||||||
ctx context.Context, orgID string, cloudProvider string,
|
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID.StringValue(), cloudProvider)
|
||||||
) ([]*types.Dashboard, *model.ApiError) {
|
|
||||||
|
|
||||||
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID, cloudProvider)
|
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
|
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
|
||||||
}
|
}
|
||||||
@ -554,7 +533,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
|||||||
for _, ar := range accountRecords {
|
for _, ar := range accountRecords {
|
||||||
if ar.AccountID != nil {
|
if ar.AccountID != nil {
|
||||||
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
|
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
|
||||||
ctx, orgID, ar.ID.StringValue(),
|
ctx, orgID.StringValue(), ar.ID.StringValue(),
|
||||||
)
|
)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
@ -573,16 +552,15 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
|||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
|
|
||||||
svcDashboards := []*types.Dashboard{}
|
svcDashboards := []*dashboardtypes.Dashboard{}
|
||||||
for _, svc := range allServices {
|
for _, svc := range allServices {
|
||||||
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
|
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
|
||||||
if serviceDashboardsCreatedAt != nil {
|
if serviceDashboardsCreatedAt != nil {
|
||||||
for _, d := range svc.Assets.Dashboards {
|
for _, d := range svc.Assets.Dashboards {
|
||||||
isLocked := 1
|
|
||||||
author := fmt.Sprintf("%s-integration", cloudProvider)
|
author := fmt.Sprintf("%s-integration", cloudProvider)
|
||||||
svcDashboards = append(svcDashboards, &types.Dashboard{
|
svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{
|
||||||
UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
|
ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
|
||||||
Locked: &isLocked,
|
Locked: true,
|
||||||
Data: *d.Definition,
|
Data: *d.Definition,
|
||||||
TimeAuditable: types.TimeAuditable{
|
TimeAuditable: types.TimeAuditable{
|
||||||
CreatedAt: *serviceDashboardsCreatedAt,
|
CreatedAt: *serviceDashboardsCreatedAt,
|
||||||
@ -592,6 +570,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
|||||||
CreatedBy: author,
|
CreatedBy: author,
|
||||||
UpdatedBy: author,
|
UpdatedBy: author,
|
||||||
},
|
},
|
||||||
|
OrgID: orgID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
servicesWithAvailableMetrics[svc.Id] = nil
|
servicesWithAvailableMetrics[svc.Id] = nil
|
||||||
@ -600,11 +579,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
|
|||||||
|
|
||||||
return svcDashboards, nil
|
return svcDashboards, nil
|
||||||
}
|
}
|
||||||
func (c *Controller) GetDashboardById(
|
func (c *Controller) GetDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
|
||||||
ctx context.Context,
|
|
||||||
orgId string,
|
|
||||||
dashboardUuid string,
|
|
||||||
) (*types.Dashboard, *model.ApiError) {
|
|
||||||
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
|
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
@ -612,38 +587,28 @@ func (c *Controller) GetDashboardById(
|
|||||||
|
|
||||||
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider)
|
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
return nil, model.WrapApiError(
|
return nil, model.WrapApiError(apiErr, "couldn't list available dashboards")
|
||||||
apiErr, fmt.Sprintf("couldn't list available dashboards"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range allDashboards {
|
for _, d := range allDashboards {
|
||||||
if d.UUID == dashboardUuid {
|
if d.ID == dashboardUuid {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, model.NotFoundError(fmt.Errorf(
|
return nil, model.NotFoundError(fmt.Errorf("couldn't find dashboard with uuid: %s", dashboardUuid))
|
||||||
"couldn't find dashboard with uuid: %s", dashboardUuid,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) dashboardUuid(
|
func (c *Controller) dashboardUuid(
|
||||||
cloudProvider string, svcId string, dashboardId string,
|
cloudProvider string, svcId string, dashboardId string,
|
||||||
) string {
|
) string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
|
||||||
"cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) parseDashboardUuid(dashboardUuid string) (
|
func (c *Controller) parseDashboardUuid(dashboardUuid string) (cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError) {
|
||||||
cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError,
|
|
||||||
) {
|
|
||||||
parts := strings.SplitN(dashboardUuid, "--", 4)
|
parts := strings.SplitN(dashboardUuid, "--", 4)
|
||||||
if len(parts) != 4 || parts[0] != "cloud-integration" {
|
if len(parts) != 4 || parts[0] != "cloud-integration" {
|
||||||
return "", "", "", model.BadRequest(fmt.Errorf(
|
return "", "", "", model.BadRequest(fmt.Errorf("invalid cloud integration dashboard id"))
|
||||||
"invalid cloud integration dashboard id",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts[1], parts[2], parts[3], nil
|
return parts[1], parts[2], parts[3], nil
|
||||||
|
@ -3,17 +3,23 @@ package cloudintegrations
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/emailing"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
|
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
|
||||||
|
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
|
||||||
|
"github.com/SigNoz/signoz/pkg/emailing/emailingtest"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||||
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -24,11 +30,16 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
|
|||||||
controller, err := NewController(sqlStore)
|
controller, err := NewController(sqlStore)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
|
||||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
|
require.NoError(err)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
|
||||||
|
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
|
||||||
|
require.NoError(err)
|
||||||
|
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||||
|
emailing := emailingtest.New()
|
||||||
|
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
|
||||||
|
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
// should be able to generate connection url for
|
// should be able to generate connection url for
|
||||||
@ -74,11 +85,17 @@ func TestAgentCheckIns(t *testing.T) {
|
|||||||
sqlStore := utils.NewQueryServiceDBForTests(t)
|
sqlStore := utils.NewQueryServiceDBForTests(t)
|
||||||
controller, err := NewController(sqlStore)
|
controller, err := NewController(sqlStore)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
|
||||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
|
require.NoError(err)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
|
||||||
|
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
|
||||||
|
require.NoError(err)
|
||||||
|
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||||
|
emailing := emailingtest.New()
|
||||||
|
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
|
||||||
|
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
// An agent should be able to check in from a cloud account even
|
// An agent should be able to check in from a cloud account even
|
||||||
@ -164,11 +181,16 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
|
|||||||
controller, err := NewController(sqlStore)
|
controller, err := NewController(sqlStore)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
|
||||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
|
require.NoError(err)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
|
||||||
|
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
|
||||||
|
require.NoError(err)
|
||||||
|
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||||
|
emailing := emailingtest.New()
|
||||||
|
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
|
||||||
|
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
// Attempting to disconnect a non-existent account should return error
|
// Attempting to disconnect a non-existent account should return error
|
||||||
@ -186,11 +208,16 @@ func TestConfigureService(t *testing.T) {
|
|||||||
controller, err := NewController(sqlStore)
|
controller, err := NewController(sqlStore)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
|
||||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||||
emailing, _ := noopemailing.New(context.Background(), providerSettings, emailing.Config{})
|
sharder, err := noopsharder.New(context.TODO(), providerSettings, sharder.Config{})
|
||||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore, providerSettings), nil, emailing, providerSettings)
|
require.NoError(err)
|
||||||
user, apiErr := createTestUser(organizationModule, userModule)
|
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlStore), sharder)
|
||||||
|
alertmanager, err := signozalertmanager.New(context.TODO(), providerSettings, alertmanager.Config{Provider: "signoz", Signoz: alertmanager.Signoz{PollInterval: 10 * time.Second, Config: alertmanagerserver.NewConfig()}}, sqlStore, orgGetter)
|
||||||
|
require.NoError(err)
|
||||||
|
jwt := authtypes.NewJWT("", 1*time.Hour, 1*time.Hour)
|
||||||
|
emailing := emailingtest.New()
|
||||||
|
modules := signoz.NewModules(sqlStore, jwt, emailing, providerSettings, orgGetter, alertmanager)
|
||||||
|
user, apiErr := createTestUser(modules.OrgSetter, modules.User)
|
||||||
require.Nil(apiErr)
|
require.Nil(apiErr)
|
||||||
|
|
||||||
// create a connected account
|
// create a connected account
|
||||||
@ -305,7 +332,7 @@ func makeTestConnectedAccount(t *testing.T, orgId string, controller *Controller
|
|||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestUser(organizationModule organization.Module, userModule user.Module) (*types.User, *model.ApiError) {
|
func createTestUser(organizationModule organization.Setter, userModule user.Module) (*types.User, *model.ApiError) {
|
||||||
// Create a test user for auth
|
// Create a test user for auth
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
organization := types.NewOrganization("test")
|
organization := types.NewOrganization("test")
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user