From a9618886b913d18620ab279d1e66423a4a25d5bf Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Wed, 12 Mar 2025 17:18:11 +0530 Subject: [PATCH] fix: support multitenancy in dashboards & savedviews (#7237) * fix: support multitenancy in dashboards * fix: support multitenancy in saved views * fix: move migrations to provider file * fix: remove getUserFromClaims and use new errors * fix: remove getUserFromClaims and use new errors * fix: use new errors in dashboards.go * Update ee/query-service/app/api/dashboard.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix: minor changes --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- ee/query-service/app/api/dashboard.go | 35 +-- ee/query-service/app/server.go | 4 +- .../src/providers/Dashboard/Dashboard.tsx | 6 +- frontend/src/types/api/dashboard/getAll.ts | 8 +- pkg/errors/code.go | 1 + pkg/errors/type.go | 1 + .../app/cloudintegrations/controller.go | 34 +-- .../app/cloudintegrations/model.go | 14 +- pkg/query-service/app/dashboards/model.go | 205 +++++------------- pkg/query-service/app/dashboards/provision.go | 85 -------- pkg/query-service/app/explorer/db.go | 112 +++++----- pkg/query-service/app/http_handler.go | 130 ++++++++--- .../app/integrations/controller.go | 6 +- pkg/query-service/app/integrations/manager.go | 52 +++-- .../app/integrations/test_utils.go | 6 +- .../app/metricsexplorer/summary.go | 13 +- pkg/query-service/app/server.go | 4 +- pkg/query-service/auth/rbac.go | 2 + pkg/query-service/common/user.go | 16 -- .../integration/signoz_integrations_test.go | 11 +- pkg/query-service/utils/testutils.go | 3 +- pkg/signoz/provider.go | 1 + .../015_update_dashboards_savedviews.go | 80 +++++++ pkg/types/dashboard.go | 65 +++++- pkg/types/savedview.go | 23 +- 25 files changed, 478 insertions(+), 439 deletions(-) delete mode 100644 pkg/query-service/app/dashboards/provision.go delete mode 100644 pkg/query-service/common/user.go create mode 100644 pkg/sqlmigration/015_update_dashboards_savedviews.go diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go index 51fe6c2ded..61c78dfe4d 100644 --- a/ee/query-service/app/api/dashboard.go +++ b/ee/query-service/app/api/dashboard.go @@ -1,15 +1,15 @@ package api import ( - "errors" "net/http" "strings" "github.com/gorilla/mux" + "go.signoz.io/signoz/pkg/errors" + "go.signoz.io/signoz/pkg/http/render" "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/auth" - "go.signoz.io/signoz/pkg/query-service/common" - "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types/authtypes" ) func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) { @@ -31,26 +31,31 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request // Get the dashboard UUID from the request uuid := mux.Vars(r)["uuid"] - if strings.HasPrefix(uuid,"integration") { - RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard") - return - } - dashboard, err := dashboards.GetDashboard(r.Context(), uuid) - if err != nil { - RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) + if strings.HasPrefix(uuid, "integration") { + render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified")) return } - user := common.GetUserFromContext(r.Context()) - if !auth.IsAdmin(user) && (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) { - RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard") + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")) + return + } + dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid) + if err != nil { + render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard")) + return + } + + if !auth.IsAdminV2(claims) && (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 = dashboards.LockUnlockDashboard(r.Context(), uuid, lock) + err = dashboards.LockUnlockDashboard(r.Context(), claims.OrgID, uuid, lock) if err != nil { - RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) + render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to lock/unlock dashboard")) return } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 4346747a0c..e40fa0a94a 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -112,7 +112,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - if err := baseexplorer.InitWithDSN(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { + if err := baseexplorer.InitWithDSN(serverOptions.SigNoz.SQLStore.BunDB()); err != nil { return nil, err } @@ -120,7 +120,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { + if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.BunDB()); err != nil { return nil, err } diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx index 5bd6d46173..f2699ad76c 100644 --- a/frontend/src/providers/Dashboard/Dashboard.tsx +++ b/frontend/src/providers/Dashboard/Dashboard.tsx @@ -273,7 +273,7 @@ export function DashboardProvider({ refetchOnWindowFocus: false, onSuccess: (data) => { const updatedDashboardData = transformDashboardVariables(data); - const updatedDate = dayjs(updatedDashboardData.updated_at); + const updatedDate = dayjs(updatedDashboardData.updatedAt); setIsDashboardLocked(updatedDashboardData?.isLocked || false); @@ -321,7 +321,7 @@ export function DashboardProvider({ dashboardRef.current = updatedDashboardData; - updatedTimeRef.current = dayjs(updatedDashboardData.updated_at); + updatedTimeRef.current = dayjs(updatedDashboardData.updatedAt); setLayouts( sortLayout(getUpdatedLayout(updatedDashboardData.data.layout)), @@ -334,7 +334,7 @@ export function DashboardProvider({ modalRef.current = modal; } else { // normal flow - updatedTimeRef.current = dayjs(updatedDashboardData.updated_at); + updatedTimeRef.current = dayjs(updatedDashboardData.updatedAt); dashboardRef.current = updatedDashboardData; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index c73884c926..0bf3020d62 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -46,10 +46,10 @@ export interface IDashboardVariable { export interface Dashboard { id: number; uuid: string; - created_at: string; - updated_at: string; - created_by: string; - updated_by: string; + createdAt: string; + updatedAt: string; + createdBy: string; + updatedBy: string; data: DashboardData; isLocked?: boolean; } diff --git a/pkg/errors/code.go b/pkg/errors/code.go index 84df6a2f5b..3da31f37e2 100644 --- a/pkg/errors/code.go +++ b/pkg/errors/code.go @@ -13,6 +13,7 @@ var ( CodeMethodNotAllowed = code{"method_not_allowed"} CodeAlreadyExists = code{"already_exists"} CodeUnauthenticated = code{"unauthenticated"} + CodeForbidden = code{"forbidden"} ) var ( diff --git a/pkg/errors/type.go b/pkg/errors/type.go index 6800a8bc71..2afdf4573e 100644 --- a/pkg/errors/type.go +++ b/pkg/errors/type.go @@ -8,6 +8,7 @@ var ( TypeMethodNotAllowed = typ{"method-not-allowed"} TypeAlreadyExists = typ{"already-exists"} TypeUnauthenticated = typ{"unauthenticated"} + TypeForbidden = typ{"forbidden"} ) // Defines custom error types diff --git a/pkg/query-service/app/cloudintegrations/controller.go b/pkg/query-service/app/cloudintegrations/controller.go index ba94ebe241..c26af1d79d 100644 --- a/pkg/query-service/app/cloudintegrations/controller.go +++ b/pkg/query-service/app/cloudintegrations/controller.go @@ -8,9 +8,9 @@ import ( "strings" "time" - "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/sqlstore" + "go.signoz.io/signoz/pkg/types" "golang.org/x/exp/maps" ) @@ -493,9 +493,9 @@ func (c *Controller) UpdateServiceConfig( // All dashboards that are available based on cloud integrations configuration // across all cloud providers func (c *Controller) AvailableDashboards(ctx context.Context) ( - []dashboards.Dashboard, *model.ApiError, + []types.Dashboard, *model.ApiError, ) { - allDashboards := []dashboards.Dashboard{} + allDashboards := []types.Dashboard{} for _, provider := range []string{"aws"} { providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, provider) @@ -513,7 +513,7 @@ func (c *Controller) AvailableDashboards(ctx context.Context) ( func (c *Controller) AvailableDashboardsForCloudProvider( ctx context.Context, cloudProvider string, -) ([]dashboards.Dashboard, *model.ApiError) { +) ([]types.Dashboard, *model.ApiError) { accountRecords, apiErr := c.accountsRepo.listConnected(ctx, cloudProvider) if apiErr != nil { @@ -545,21 +545,25 @@ func (c *Controller) AvailableDashboardsForCloudProvider( return nil, apiErr } - svcDashboards := []dashboards.Dashboard{} + svcDashboards := []types.Dashboard{} for _, svc := range allServices { serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id] if serviceDashboardsCreatedAt != nil { for _, d := range svc.Assets.Dashboards { isLocked := 1 author := fmt.Sprintf("%s-integration", cloudProvider) - svcDashboards = append(svcDashboards, dashboards.Dashboard{ - Uuid: c.dashboardUuid(cloudProvider, svc.Id, d.Id), - Locked: &isLocked, - Data: *d.Definition, - CreatedAt: *serviceDashboardsCreatedAt, - CreateBy: &author, - UpdatedAt: *serviceDashboardsCreatedAt, - UpdateBy: &author, + svcDashboards = append(svcDashboards, types.Dashboard{ + UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id), + Locked: &isLocked, + Data: *d.Definition, + TimeAuditable: types.TimeAuditable{ + CreatedAt: *serviceDashboardsCreatedAt, + UpdatedAt: *serviceDashboardsCreatedAt, + }, + UserAuditable: types.UserAuditable{ + CreatedBy: author, + UpdatedBy: author, + }, }) } servicesWithAvailableMetrics[svc.Id] = nil @@ -571,7 +575,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider( func (c *Controller) GetDashboardById( ctx context.Context, dashboardUuid string, -) (*dashboards.Dashboard, *model.ApiError) { +) (*types.Dashboard, *model.ApiError) { cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid) if apiErr != nil { return nil, apiErr @@ -585,7 +589,7 @@ func (c *Controller) GetDashboardById( } for _, d := range allDashboards { - if d.Uuid == dashboardUuid { + if d.UUID == dashboardUuid { return &d, nil } } diff --git a/pkg/query-service/app/cloudintegrations/model.go b/pkg/query-service/app/cloudintegrations/model.go index 909d13c5ab..63c33a274f 100644 --- a/pkg/query-service/app/cloudintegrations/model.go +++ b/pkg/query-service/app/cloudintegrations/model.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "go.signoz.io/signoz/pkg/query-service/app/dashboards" + "go.signoz.io/signoz/pkg/types" ) // Represents a cloud provider account for cloud integrations @@ -187,12 +187,12 @@ type CloudServiceAssets struct { } type CloudServiceDashboard struct { - Id string `json:"id"` - Url string `json:"url"` - Title string `json:"title"` - Description string `json:"description"` - Image string `json:"image"` - Definition *dashboards.Data `json:"definition,omitempty"` + Id string `json:"id"` + Url string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` + Definition *types.DashboardData `json:"definition,omitempty"` } type SupportedSignals struct { diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index 416feaac02..fde3d31936 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -2,7 +2,6 @@ package dashboards import ( "context" - "encoding/base64" "encoding/json" "fmt" "regexp" @@ -11,18 +10,17 @@ import ( "time" "github.com/google/uuid" - "github.com/gosimple/slug" - "github.com/jmoiron/sqlx" - "go.signoz.io/signoz/pkg/query-service/common" + "github.com/uptrace/bun" "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/query-service/telemetry" "go.uber.org/zap" ) // This time the global variable is unexported. -var db *sqlx.DB +var db *bun.DB // User for mapping job,instance from grafana var ( @@ -35,96 +33,43 @@ var ( ) // InitDB sets up setting up the connection pool global variable. -func InitDB(inputDB *sqlx.DB) error { +func InitDB(inputDB *bun.DB) error { db = inputDB telemetry.GetInstance().SetDashboardsInfoCallback(GetDashboardsInfo) return nil } -type Dashboard struct { - Id int `json:"id" db:"id"` - Uuid string `json:"uuid" db:"uuid"` - Slug string `json:"-" db:"-"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - CreateBy *string `json:"created_by" db:"created_by"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - UpdateBy *string `json:"updated_by" db:"updated_by"` - Title string `json:"-" db:"-"` - Data Data `json:"data" db:"data"` - Locked *int `json:"isLocked" db:"locked"` -} - -type Data map[string]interface{} - -// func (c *Data) Value() (driver.Value, error) { -// if c != nil { -// b, err := json.Marshal(c) -// if err != nil { -// return nil, err -// } -// return string(b), nil -// } -// return nil, nil -// } - -func (c *Data) Scan(src interface{}) error { - var data []byte - if b, ok := src.([]byte); ok { - data = b - } else if s, ok := src.(string); ok { - data = []byte(s) - } - return json.Unmarshal(data, c) -} - // CreateDashboard creates a new dashboard -func CreateDashboard(ctx context.Context, data map[string]interface{}, fm interfaces.FeatureLookup) (*Dashboard, *model.ApiError) { - dash := &Dashboard{ +func CreateDashboard(ctx context.Context, orgID string, email string, data map[string]interface{}, fm interfaces.FeatureLookup) (*types.Dashboard, *model.ApiError) { + dash := &types.Dashboard{ Data: data, } - var userEmail string - if user := common.GetUserFromContext(ctx); user != nil { - userEmail = user.Email - } + + dash.OrgID = orgID dash.CreatedAt = time.Now() - dash.CreateBy = &userEmail + dash.CreatedBy = email dash.UpdatedAt = time.Now() - dash.UpdateBy = &userEmail + dash.UpdatedBy = email dash.UpdateSlug() - dash.Uuid = uuid.New().String() + dash.UUID = uuid.New().String() if data["uuid"] != nil { - dash.Uuid = data["uuid"].(string) + dash.UUID = data["uuid"].(string) } - mapData, err := json.Marshal(dash.Data) - if err != nil { - zap.L().Error("Error in marshalling data field in dashboard: ", zap.Any("dashboard", dash), zap.Error(err)) - return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} - } - - result, err := db.Exec("INSERT INTO dashboards (uuid, created_at, created_by, updated_at, updated_by, data) VALUES ($1, $2, $3, $4, $5, $6)", - dash.Uuid, dash.CreatedAt, userEmail, dash.UpdatedAt, userEmail, mapData) - + err := db.NewInsert().Model(dash).Returning("id").Scan(ctx, &dash.ID) if err != nil { zap.L().Error("Error in inserting dashboard data: ", zap.Any("dashboard", dash), zap.Error(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} } - lastInsertId, err := result.LastInsertId() - if err != nil { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} - } - dash.Id = int(lastInsertId) return dash, nil } -func GetDashboards(ctx context.Context) ([]Dashboard, *model.ApiError) { +func GetDashboards(ctx context.Context, orgID string) ([]types.Dashboard, *model.ApiError) { + dashboards := []types.Dashboard{} - dashboards := []Dashboard{} - query := `SELECT * FROM dashboards` - - err := db.Select(&dashboards, query) + err := db.NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} } @@ -132,23 +77,19 @@ func GetDashboards(ctx context.Context) ([]Dashboard, *model.ApiError) { return dashboards, nil } -func DeleteDashboard(ctx context.Context, uuid string, fm interfaces.FeatureLookup) *model.ApiError { +func DeleteDashboard(ctx context.Context, orgID, uuid string, fm interfaces.FeatureLookup) *model.ApiError { - dashboard, dErr := GetDashboard(ctx, uuid) + dashboard, dErr := GetDashboard(ctx, orgID, uuid) if dErr != nil { zap.L().Error("Error in getting dashboard: ", zap.String("uuid", uuid), zap.Any("error", dErr)) return dErr } - if user := common.GetUserFromContext(ctx); user != nil { - if dashboard.Locked != nil && *dashboard.Locked == 1 { - return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it")) - } + if dashboard.Locked != nil && *dashboard.Locked == 1 { + return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it")) } - query := `DELETE FROM dashboards WHERE uuid=?` - - result, err := db.Exec(query, uuid) + result, err := db.NewDelete().Model(&types.Dashboard{}).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx) if err != nil { return &model.ApiError{Typ: model.ErrorExec, Err: err} } @@ -164,12 +105,10 @@ func DeleteDashboard(ctx context.Context, uuid string, fm interfaces.FeatureLook return nil } -func GetDashboard(ctx context.Context, uuid string) (*Dashboard, *model.ApiError) { +func GetDashboard(ctx context.Context, orgID, uuid string) (*types.Dashboard, *model.ApiError) { - dashboard := Dashboard{} - query := `SELECT * FROM dashboards WHERE uuid=?` - - err := db.Get(&dashboard, query, uuid) + dashboard := types.Dashboard{} + err := db.NewSelect().Model(&dashboard).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Scan(ctx) if err != nil { return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no dashboard found with uuid: %s", uuid)} } @@ -177,7 +116,7 @@ func GetDashboard(ctx context.Context, uuid string) (*Dashboard, *model.ApiError return &dashboard, nil } -func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface{}, fm interfaces.FeatureLookup) (*Dashboard, *model.ApiError) { +func UpdateDashboard(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}, fm interfaces.FeatureLookup) (*types.Dashboard, *model.ApiError) { mapData, err := json.Marshal(data) if err != nil { @@ -185,17 +124,13 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err} } - dashboard, apiErr := GetDashboard(ctx, uuid) + dashboard, apiErr := GetDashboard(ctx, orgID, uuid) if apiErr != nil { return nil, apiErr } - var userEmail string - if user := common.GetUserFromContext(ctx); user != nil { - userEmail = user.Email - if dashboard.Locked != nil && *dashboard.Locked == 1 { - return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it")) - } + if dashboard.Locked != nil && *dashboard.Locked == 1 { + return nil, model.BadRequest(fmt.Errorf("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, @@ -210,11 +145,10 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface } dashboard.UpdatedAt = time.Now() - dashboard.UpdateBy = &userEmail + dashboard.UpdatedBy = userEmail dashboard.Data = data - _, err = db.Exec("UPDATE dashboards SET updated_at=$1, updated_by=$2, data=$3 WHERE uuid=$4;", - dashboard.UpdatedAt, userEmail, mapData, dashboard.Uuid) + _, err = db.NewUpdate().Model(dashboard).Set("updated_at = ?", dashboard.UpdatedAt).Set("updated_by = ?", userEmail).Set("data = ?", mapData).Where("uuid = ?", dashboard.UUID).Exec(ctx) if err != nil { zap.L().Error("Error in inserting dashboard data", zap.Any("data", data), zap.Error(err)) @@ -223,16 +157,20 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface return dashboard, nil } -func LockUnlockDashboard(ctx context.Context, uuid string, lock bool) *model.ApiError { - var query string - if lock { - query = `UPDATE dashboards SET locked=1 WHERE uuid=?;` - } else { - query = `UPDATE dashboards SET locked=0 WHERE uuid=?;` +func LockUnlockDashboard(ctx context.Context, orgID, uuid string, lock bool) *model.ApiError { + dashboard, apiErr := GetDashboard(ctx, orgID, uuid) + if apiErr != nil { + return apiErr } - _, err := db.Exec(query, uuid) + var lockValue int + if lock { + lockValue = 1 + } else { + lockValue = 0 + } + _, err := db.NewUpdate().Model(dashboard).Set("locked = ?", lockValue).Where("org_id = ?", orgID).Where("uuid = ?", uuid).Exec(ctx) if err != nil { zap.L().Error("Error in updating dashboard", zap.String("uuid", uuid), zap.Error(err)) return &model.ApiError{Typ: model.ErrorExec, Err: err} @@ -241,17 +179,6 @@ func LockUnlockDashboard(ctx context.Context, uuid string, lock bool) *model.Api return nil } -// UpdateSlug updates the slug -func (d *Dashboard) UpdateSlug() { - var title string - - if val, ok := d.Data["title"]; ok { - title = val.(string) - } - - d.Slug = SlugifyTitle(title) -} - func IsPostDataSane(data *map[string]interface{}) error { val, ok := (*data)["title"] if !ok || val == nil { @@ -261,21 +188,6 @@ func IsPostDataSane(data *map[string]interface{}) error { return nil } -func SlugifyTitle(title string) string { - s := slug.Make(strings.ToLower(title)) - if s == "" { - // If the dashboard name is only characters outside of the - // sluggable characters, the slug creation will return an - // empty string which will mess up URLs. This failsafe picks - // that up and creates the slug as a base64 identifier instead. - s = base64.RawURLEncoding.EncodeToString([]byte(title)) - if slug.MaxLength != 0 && len(s) > slug.MaxLength { - s = s[:slug.MaxLength] - } - } - return s -} - func getWidgetIds(data map[string]interface{}) []string { widgetIds := []string{} if data != nil && data["widgets"] != nil { @@ -329,9 +241,8 @@ func getIdDifference(existingIds []string, newIds []string) []string { func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) { dashboardsInfo := model.DashboardsInfo{} // fetch dashboards from dashboard db - query := "SELECT data FROM dashboards" - var dashboardsData []Dashboard - err := db.Select(&dashboardsData, query) + dashboards := []types.Dashboard{} + err := db.NewSelect().Model(&dashboards).Scan(ctx) if err != nil { zap.L().Error("Error in processing sql query", zap.Error(err)) return &dashboardsInfo, err @@ -340,7 +251,7 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) { var dashboardNames []string count := 0 queriesWithTagAttrs := 0 - for _, dashboard := range dashboardsData { + for _, dashboard := range dashboards { if isDashboardWithPanelAndName(dashboard.Data) { totalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName + 1 } @@ -371,7 +282,7 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) { } dashboardsInfo.DashboardNames = dashboardNames - dashboardsInfo.TotalDashboards = len(dashboardsData) + dashboardsInfo.TotalDashboards = len(dashboards) dashboardsInfo.TotalDashboardsWithPanelAndName = totalDashboardsWithPanelAndName dashboardsInfo.QueriesWithTSV2 = count dashboardsInfo.QueriesWithTagAttrs = queriesWithTagAttrs @@ -538,17 +449,13 @@ func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsIn } } -func GetDashboardsWithMetricNames(ctx context.Context, metricNames []string) (map[string][]map[string]string, *model.ApiError) { - // Get all dashboards first - query := `SELECT uuid, data FROM dashboards` - - type dashboardRow struct { - Uuid string `db:"uuid"` - Data json.RawMessage `db:"data"` +func GetDashboardsWithMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, *model.ApiError) { + dashboards := []types.Dashboard{} + err := db.NewSelect().Model(&dashboards).Where("org_id = ?", orgID).Scan(ctx) + if err != nil { + zap.L().Error("Error in getting dashboards", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} } - - var dashboards []dashboardRow - err := db.Select(&dashboards, query) if err != nil { zap.L().Error("Error in getting dashboards", zap.Error(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} @@ -556,16 +463,10 @@ func GetDashboardsWithMetricNames(ctx context.Context, metricNames []string) (ma // Initialize result map for each metric result := make(map[string][]map[string]string) - // for _, metricName := range metricNames { - // result[metricName] = []map[string]string{} - // } // Process the JSON data in Go for _, dashboard := range dashboards { - var dashData map[string]interface{} - if err := json.Unmarshal(dashboard.Data, &dashData); err != nil { - continue - } + var dashData = dashboard.Data dashTitle, _ := dashData["title"].(string) widgets, ok := dashData["widgets"].([]interface{}) @@ -617,7 +518,7 @@ func GetDashboardsWithMetricNames(ctx context.Context, metricNames []string) (ma for _, metricName := range metricNames { if strings.TrimSpace(key) == metricName { result[metricName] = append(result[metricName], map[string]string{ - "dashboard_id": dashboard.Uuid, + "dashboard_id": dashboard.UUID, "widget_title": widgetTitle, "widget_id": widgetID, "dashboard_title": dashTitle, diff --git a/pkg/query-service/app/dashboards/provision.go b/pkg/query-service/app/dashboards/provision.go deleted file mode 100644 index 9c12f66b1d..0000000000 --- a/pkg/query-service/app/dashboards/provision.go +++ /dev/null @@ -1,85 +0,0 @@ -package dashboards - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "strings" - - "go.uber.org/zap" - - "go.signoz.io/signoz/pkg/query-service/constants" - "go.signoz.io/signoz/pkg/query-service/interfaces" - "go.signoz.io/signoz/pkg/query-service/model" -) - -func readCurrentDir(dir string, fm interfaces.FeatureLookup) error { - file, err := os.Open(dir) - if err != nil { - zap.L().Warn("failed opening directory", zap.Error(err)) - return nil - } - defer file.Close() - - list, _ := file.Readdirnames(0) // 0 to read all files and folders - for _, filename := range list { - if strings.ToLower(filepath.Ext(filename)) != ".json" { - zap.L().Debug("Skipping non-json file", zap.String("filename", filename)) - continue - } - zap.L().Info("Provisioning dashboard: ", zap.String("filename", filename)) - - // using filepath.Join for platform specific path creation - // which is equivalent to "dir+/+filename" (on unix based systems) but cleaner - plan, err := os.ReadFile(filepath.Join(dir, filename)) - if err != nil { - zap.L().Error("Creating Dashboards: Error in reading json fron file", zap.String("filename", filename), zap.Error(err)) - continue - } - var data map[string]interface{} - err = json.Unmarshal(plan, &data) - if err != nil { - zap.L().Error("Creating Dashboards: Error in unmarshalling json from file", zap.String("filename", filename), zap.Error(err)) - continue - } - err = IsPostDataSane(&data) - if err != nil { - zap.L().Info("Creating Dashboards: Error in file", zap.String("filename", filename), zap.Error(err)) - continue - } - - id := data["uuid"] - if id == nil { - _, apiErr := CreateDashboard(context.Background(), data, fm) - if apiErr != nil { - zap.L().Error("Creating Dashboards: Error in file", zap.String("filename", filename), zap.Error(apiErr.Err)) - } - continue - } - - apiErr := upsertDashboard(id.(string), data, filename, fm) - if apiErr != nil { - zap.L().Error("Creating Dashboards: Error upserting dashboard", zap.String("filename", filename), zap.Error(apiErr.Err)) - } - } - return nil -} - -func upsertDashboard(uuid string, data map[string]interface{}, filename string, fm interfaces.FeatureLookup) *model.ApiError { - _, apiErr := GetDashboard(context.Background(), uuid) - if apiErr == nil { - zap.S().Infof("Creating Dashboards: Already exists: %s\t%s", filename, "Dashboard already present in database, Updating dashboard") - _, apiErr := UpdateDashboard(context.Background(), uuid, data, fm) - return apiErr - } - - zap.S().Infof("Creating Dashboards: UUID not found: %s\t%s", filename, "Dashboard not present in database, Creating dashboard") - _, apiErr = CreateDashboard(context.Background(), data, fm) - return apiErr -} - -func LoadDashboardFiles(fm interfaces.FeatureLookup) error { - dashboardsPath := constants.GetOrDefaultEnv("DASHBOARDS_PATH", "./config/dashboards") - return readCurrentDir(dashboardsPath, fm) -} diff --git a/pkg/query-service/app/explorer/db.go b/pkg/query-service/app/explorer/db.go index acb7aefc15..7320e799e7 100644 --- a/pkg/query-service/app/explorer/db.go +++ b/pkg/query-service/app/explorer/db.go @@ -9,45 +9,32 @@ import ( "time" "github.com/google/uuid" - "github.com/jmoiron/sqlx" + "github.com/uptrace/bun" "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/telemetry" + "go.signoz.io/signoz/pkg/types" "go.signoz.io/signoz/pkg/types/authtypes" "go.uber.org/zap" ) -var db *sqlx.DB - -type SavedView struct { - UUID string `json:"uuid" db:"uuid"` - Name string `json:"name" db:"name"` - Category string `json:"category" db:"category"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - CreatedBy string `json:"created_by" db:"created_by"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - UpdatedBy string `json:"updated_by" db:"updated_by"` - SourcePage string `json:"source_page" db:"source_page"` - Tags string `json:"tags" db:"tags"` - Data string `json:"data" db:"data"` - ExtraData string `json:"extra_data" db:"extra_data"` -} +var db *bun.DB // InitWithDSN sets up setting up the connection pool global variable. -func InitWithDSN(inputDB *sqlx.DB) error { +func InitWithDSN(inputDB *bun.DB) error { db = inputDB telemetry.GetInstance().SetSavedViewsInfoCallback(GetSavedViewsInfo) return nil } -func InitWithDB(sqlDB *sqlx.DB) { - db = sqlDB +func InitWithDB(bunDB *bun.DB) { + db = bunDB } -func GetViews() ([]*v3.SavedView, error) { - var views []SavedView - err := db.Select(&views, "SELECT * FROM saved_views") +func GetViews(ctx context.Context, orgID string) ([]*v3.SavedView, error) { + var views []types.SavedView + err := db.NewSelect().Model(&views).Where("org_id = ?", orgID).Scan(ctx) if err != nil { return nil, fmt.Errorf("error in getting saved views: %s", err.Error()) } @@ -76,13 +63,13 @@ func GetViews() ([]*v3.SavedView, error) { return savedViews, nil } -func GetViewsForFilters(sourcePage string, name string, category string) ([]*v3.SavedView, error) { - var views []SavedView +func GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) { + var views []types.SavedView var err error if len(category) == 0 { - err = db.Select(&views, "SELECT * FROM saved_views WHERE source_page = ? AND name LIKE ?", sourcePage, "%"+name+"%") + err = db.NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx) } else { - err = db.Select(&views, "SELECT * FROM saved_views WHERE source_page = ? AND category LIKE ? AND name LIKE ?", sourcePage, "%"+category+"%", "%"+name+"%") + err = db.NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND category LIKE ? AND name LIKE ?", orgID, sourcePage, "%"+category+"%", "%"+name+"%").Scan(ctx) } if err != nil { return nil, fmt.Errorf("error in getting saved views: %s", err.Error()) @@ -111,7 +98,7 @@ func GetViewsForFilters(sourcePage string, name string, category string) ([]*v3. return savedViews, nil } -func CreateView(ctx context.Context, view v3.SavedView) (string, error) { +func CreateView(ctx context.Context, orgID string, view v3.SavedView) (string, error) { data, err := json.Marshal(view.CompositeQuery) if err != nil { return "", fmt.Errorf("error in marshalling explorer query data: %s", err.Error()) @@ -133,29 +120,35 @@ func CreateView(ctx context.Context, view v3.SavedView) (string, error) { createBy := claims.Email updatedBy := claims.Email - _, err = db.Exec( - "INSERT INTO saved_views (uuid, name, category, created_at, created_by, updated_at, updated_by, source_page, tags, data, extra_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - uuid_, - view.Name, - view.Category, - createdAt, - createBy, - updatedAt, - updatedBy, - view.SourcePage, - strings.Join(view.Tags, ","), - data, - view.ExtraData, - ) + dbView := types.SavedView{ + TimeAuditable: types.TimeAuditable{ + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + UserAuditable: types.UserAuditable{ + CreatedBy: createBy, + UpdatedBy: updatedBy, + }, + OrgID: orgID, + UUID: uuid_, + Name: view.Name, + Category: view.Category, + SourcePage: view.SourcePage, + Tags: strings.Join(view.Tags, ","), + Data: string(data), + ExtraData: view.ExtraData, + } + + _, err = db.NewInsert().Model(&dbView).Exec(ctx) if err != nil { return "", fmt.Errorf("error in creating saved view: %s", err.Error()) } return uuid_, nil } -func GetView(uuid_ string) (*v3.SavedView, error) { - var view SavedView - err := db.Get(&view, "SELECT * FROM saved_views WHERE uuid = ?", uuid_) +func GetView(ctx context.Context, orgID string, uuid_ string) (*v3.SavedView, error) { + var view types.SavedView + err := db.NewSelect().Model(&view).Where("org_id = ? AND uuid = ?", orgID, uuid_).Scan(ctx) if err != nil { return nil, fmt.Errorf("error in getting saved view: %s", err.Error()) } @@ -180,7 +173,7 @@ func GetView(uuid_ string) (*v3.SavedView, error) { }, nil } -func UpdateView(ctx context.Context, uuid_ string, view v3.SavedView) error { +func UpdateView(ctx context.Context, orgID string, uuid_ string, view v3.SavedView) error { data, err := json.Marshal(view.CompositeQuery) if err != nil { return fmt.Errorf("error in marshalling explorer query data: %s", err.Error()) @@ -194,16 +187,25 @@ func UpdateView(ctx context.Context, uuid_ string, view v3.SavedView) error { updatedAt := time.Now() updatedBy := claims.Email - _, err = db.Exec("UPDATE saved_views SET updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ? WHERE uuid = ?", - updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData, uuid_) + _, err = db.NewUpdate(). + Model(&types.SavedView{}). + Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?", + updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData). + Where("uuid = ?", uuid_). + Where("org_id = ?", orgID). + Exec(ctx) if err != nil { return fmt.Errorf("error in updating saved view: %s", err.Error()) } return nil } -func DeleteView(uuid_ string) error { - _, err := db.Exec("DELETE FROM saved_views WHERE uuid = ?", uuid_) +func DeleteView(ctx context.Context, orgID string, uuid_ string) error { + _, err := db.NewDelete(). + Model(&types.SavedView{}). + Where("uuid = ?", uuid_). + Where("org_id = ?", orgID). + Exec(ctx) if err != nil { return fmt.Errorf("error in deleting explorer query: %s", err.Error()) } @@ -212,7 +214,17 @@ func DeleteView(uuid_ string) error { func GetSavedViewsInfo(ctx context.Context) (*model.SavedViewsInfo, error) { savedViewsInfo := model.SavedViewsInfo{} - savedViews, err := GetViews() + // get single org ID from db + var orgIDs []string + err := db.NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs) + if err != nil { + return nil, fmt.Errorf("error in getting org IDs: %s", err.Error()) + } + if len(orgIDs) != 1 { + zap.S().Warn("GetSavedViewsInfo: Zero or multiple org IDs found in the database", zap.Int("orgIDs", len(orgIDs))) + return &savedViewsInfo, nil + } + savedViews, err := GetViews(ctx, orgIDs[0]) if err != nil { zap.S().Debug("Error in fetching saved views info: ", err) return &savedViewsInfo, err diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index ecf407ac5f..6354e90a0a 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -19,6 +19,8 @@ import ( "time" "go.signoz.io/signoz/pkg/alertmanager" + errorsV2 "go.signoz.io/signoz/pkg/errors" + "go.signoz.io/signoz/pkg/http/render" "go.signoz.io/signoz/pkg/query-service/app/metricsexplorer" "go.signoz.io/signoz/pkg/signoz" @@ -49,7 +51,6 @@ import ( tracesV4 "go.signoz.io/signoz/pkg/query-service/app/traces/v4" "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/cache" - "go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/contextlinks" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" @@ -274,11 +275,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { } aH.queryBuilder = queryBuilder.NewQueryBuilder(builderOpts, aH.featureFlags) - dashboards.LoadDashboardFiles(aH.featureFlags) - // if errReadingDashboards != nil { - // return nil, errReadingDashboards - // } - // check if at least one user is created hasUsers, err := aH.appDao.GetUsersWithOpts(context.Background(), 1) if err.Error() != "" { @@ -1059,7 +1055,12 @@ func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) { - allDashboards, err := dashboards.GetDashboards(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + allDashboards, err := dashboards.GetDashboards(r.Context(), claims.OrgID) if err != nil { RespondError(w, err, nil) return @@ -1113,7 +1114,7 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) { inter = Intersection(inter, tags2Dash[tag]) } - filteredDashboards := []dashboards.Dashboard{} + filteredDashboards := []types.Dashboard{} for _, val := range inter { dash := (allDashboards)[val] filteredDashboards = append(filteredDashboards, dash) @@ -1125,7 +1126,12 @@ func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) deleteDashboard(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] - err := dashboards.DeleteDashboard(r.Context(), uuid, aH.featureFlags) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + err := dashboards.DeleteDashboard(r.Context(), claims.OrgID, uuid, aH.featureFlags) if err != nil { RespondError(w, err, nil) @@ -1212,7 +1218,12 @@ func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) { return } - dashboard, apiError := dashboards.UpdateDashboard(r.Context(), uuid, postData, aH.featureFlags) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + dashboard, apiError := dashboards.UpdateDashboard(r.Context(), claims.OrgID, claims.Email, uuid, postData, aH.featureFlags) if apiError != nil { RespondError(w, apiError, nil) return @@ -1226,7 +1237,12 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) { uuid := mux.Vars(r)["uuid"] - dashboard, apiError := dashboards.GetDashboard(r.Context(), uuid) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + dashboard, apiError := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid) if apiError != nil { if apiError.Type() != model.ErrorNotFound { @@ -1275,8 +1291,12 @@ func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body") return } - - dash, apiErr := dashboards.CreateDashboard(r.Context(), postData, aH.featureFlags) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + dash, apiErr := dashboards.CreateDashboard(r.Context(), claims.OrgID, claims.Email, postData, aH.featureFlags) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3385,10 +3405,14 @@ func (aH *APIHandler) getUserPreference( w http.ResponseWriter, r *http.Request, ) { preferenceId := mux.Vars(r)["preferenceId"] - user := common.GetUserFromContext(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } preference, apiErr := preferences.GetUserPreference( - r.Context(), preferenceId, user.User.OrgID, user.User.ID, + r.Context(), preferenceId, claims.OrgID, claims.UserID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3402,7 +3426,11 @@ func (aH *APIHandler) updateUserPreference( w http.ResponseWriter, r *http.Request, ) { preferenceId := mux.Vars(r)["preferenceId"] - user := common.GetUserFromContext(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } req := preferences.UpdatePreference{} err := json.NewDecoder(r.Body).Decode(&req) @@ -3411,7 +3439,7 @@ func (aH *APIHandler) updateUserPreference( RespondError(w, model.BadRequest(err), nil) return } - preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.ID) + preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, claims.UserID) if apiErr != nil { RespondError(w, apiErr, nil) return @@ -3423,9 +3451,13 @@ func (aH *APIHandler) updateUserPreference( func (aH *APIHandler) getAllUserPreferences( w http.ResponseWriter, r *http.Request, ) { - user := common.GetUserFromContext(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } preference, apiErr := preferences.GetAllUserPreferences( - r.Context(), user.User.OrgID, user.User.ID, + r.Context(), claims.OrgID, claims.UserID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3439,9 +3471,13 @@ func (aH *APIHandler) getOrgPreference( w http.ResponseWriter, r *http.Request, ) { preferenceId := mux.Vars(r)["preferenceId"] - user := common.GetUserFromContext(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } preference, apiErr := preferences.GetOrgPreference( - r.Context(), preferenceId, user.User.OrgID, + r.Context(), preferenceId, claims.OrgID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -3456,7 +3492,11 @@ func (aH *APIHandler) updateOrgPreference( ) { preferenceId := mux.Vars(r)["preferenceId"] req := preferences.UpdatePreference{} - user := common.GetUserFromContext(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } err := json.NewDecoder(r.Body).Decode(&req) @@ -3464,7 +3504,7 @@ func (aH *APIHandler) updateOrgPreference( RespondError(w, model.BadRequest(err), nil) return } - preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgID) + preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, claims.OrgID) if apiErr != nil { RespondError(w, apiErr, nil) return @@ -3476,9 +3516,13 @@ func (aH *APIHandler) updateOrgPreference( func (aH *APIHandler) getAllOrgPreferences( w http.ResponseWriter, r *http.Request, ) { - user := common.GetUserFromContext(r.Context()) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } preference, apiErr := preferences.GetAllOrgPreferences( - r.Context(), user.User.OrgID, + r.Context(), claims.OrgID, ) if apiErr != nil { RespondError(w, apiErr, nil) @@ -4525,7 +4569,12 @@ func (aH *APIHandler) getSavedViews(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") category := r.URL.Query().Get("category") - queries, err := explorer.GetViewsForFilters(sourcePage, name, category) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + queries, err := explorer.GetViewsForFilters(r.Context(), claims.OrgID, sourcePage, name, category) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return @@ -4545,7 +4594,13 @@ func (aH *APIHandler) createSavedViews(w http.ResponseWriter, r *http.Request) { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } - uuid, err := explorer.CreateView(r.Context(), view) + + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + uuid, err := explorer.CreateView(r.Context(), claims.OrgID, view) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return @@ -4556,7 +4611,12 @@ func (aH *APIHandler) createSavedViews(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getSavedView(w http.ResponseWriter, r *http.Request) { viewID := mux.Vars(r)["viewId"] - view, err := explorer.GetView(viewID) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + view, err := explorer.GetView(r.Context(), claims.OrgID, viewID) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return @@ -4579,7 +4639,12 @@ func (aH *APIHandler) updateSavedView(w http.ResponseWriter, r *http.Request) { return } - err = explorer.UpdateView(r.Context(), viewID, view) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + err = explorer.UpdateView(r.Context(), claims.OrgID, viewID, view) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return @@ -4591,7 +4656,12 @@ func (aH *APIHandler) updateSavedView(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) deleteSavedView(w http.ResponseWriter, r *http.Request) { viewID := mux.Vars(r)["viewId"] - err := explorer.DeleteView(viewID) + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + err := explorer.DeleteView(r.Context(), claims.OrgID, viewID) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return diff --git a/pkg/query-service/app/integrations/controller.go b/pkg/query-service/app/integrations/controller.go index 5bfee3eb2b..d2113148b0 100644 --- a/pkg/query-service/app/integrations/controller.go +++ b/pkg/query-service/app/integrations/controller.go @@ -5,10 +5,10 @@ import ( "fmt" "go.signoz.io/signoz/pkg/query-service/agentConf" - "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/sqlstore" + "go.signoz.io/signoz/pkg/types" ) type Controller struct { @@ -130,12 +130,12 @@ func (c *Controller) GetPipelinesForInstalledIntegrations( func (c *Controller) GetDashboardsForInstalledIntegrations( ctx context.Context, -) ([]dashboards.Dashboard, *model.ApiError) { +) ([]types.Dashboard, *model.ApiError) { return c.mgr.GetDashboardsForInstalledIntegrations(ctx) } func (c *Controller) GetInstalledIntegrationDashboardById( ctx context.Context, dashboardUuid string, -) (*dashboards.Dashboard, *model.ApiError) { +) (*types.Dashboard, *model.ApiError) { return c.mgr.GetInstalledIntegrationDashboardById(ctx, dashboardUuid) } diff --git a/pkg/query-service/app/integrations/manager.go b/pkg/query-service/app/integrations/manager.go index 6cd5a0c853..7e0c361308 100644 --- a/pkg/query-service/app/integrations/manager.go +++ b/pkg/query-service/app/integrations/manager.go @@ -9,11 +9,11 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" - "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/rules" "go.signoz.io/signoz/pkg/query-service/utils" + "go.signoz.io/signoz/pkg/types" ) type IntegrationAuthor struct { @@ -32,8 +32,8 @@ type IntegrationSummary struct { } type IntegrationAssets struct { - Logs LogsAssets `json:"logs"` - Dashboards []dashboards.Data `json:"dashboards"` + Logs LogsAssets `json:"logs"` + Dashboards []types.DashboardData `json:"dashboards"` Alerts []rules.PostableRule `json:"alerts"` } @@ -306,7 +306,7 @@ func (m *Manager) parseDashboardUuid(dashboardUuid string) ( func (m *Manager) GetInstalledIntegrationDashboardById( ctx context.Context, dashboardUuid string, -) (*dashboards.Dashboard, *model.ApiError) { +) (*types.Dashboard, *model.ApiError) { integrationId, dashboardId, apiErr := m.parseDashboardUuid(dashboardUuid) if apiErr != nil { return nil, apiErr @@ -328,14 +328,18 @@ func (m *Manager) GetInstalledIntegrationDashboardById( if id, ok := dId.(string); ok && id == dashboardId { isLocked := 1 author := "integration" - return &dashboards.Dashboard{ - Uuid: m.dashboardUuid(integrationId, string(dashboardId)), - Locked: &isLocked, - Data: dd, - CreatedAt: integration.Installation.InstalledAt, - CreateBy: &author, - UpdatedAt: integration.Installation.InstalledAt, - UpdateBy: &author, + return &types.Dashboard{ + UUID: m.dashboardUuid(integrationId, string(dashboardId)), + Locked: &isLocked, + Data: dd, + TimeAuditable: types.TimeAuditable{ + CreatedAt: integration.Installation.InstalledAt, + UpdatedAt: integration.Installation.InstalledAt, + }, + UserAuditable: types.UserAuditable{ + CreatedBy: author, + UpdatedBy: author, + }, }, nil } } @@ -348,13 +352,13 @@ func (m *Manager) GetInstalledIntegrationDashboardById( func (m *Manager) GetDashboardsForInstalledIntegrations( ctx context.Context, -) ([]dashboards.Dashboard, *model.ApiError) { +) ([]types.Dashboard, *model.ApiError) { installedIntegrations, apiErr := m.getInstalledIntegrations(ctx) if apiErr != nil { return nil, apiErr } - result := []dashboards.Dashboard{} + result := []types.Dashboard{} for _, ii := range installedIntegrations { for _, dd := range ii.Assets.Dashboards { @@ -362,14 +366,18 @@ func (m *Manager) GetDashboardsForInstalledIntegrations( if dashboardId, ok := dId.(string); ok { isLocked := 1 author := "integration" - result = append(result, dashboards.Dashboard{ - Uuid: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId), - Locked: &isLocked, - Data: dd, - CreatedAt: ii.Installation.InstalledAt, - CreateBy: &author, - UpdatedAt: ii.Installation.InstalledAt, - UpdateBy: &author, + result = append(result, types.Dashboard{ + UUID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId), + Locked: &isLocked, + Data: dd, + TimeAuditable: types.TimeAuditable{ + CreatedAt: ii.Installation.InstalledAt, + UpdatedAt: ii.Installation.InstalledAt, + }, + UserAuditable: types.UserAuditable{ + CreatedBy: author, + UpdatedBy: author, + }, }) } } diff --git a/pkg/query-service/app/integrations/test_utils.go b/pkg/query-service/app/integrations/test_utils.go index 18fbf1c461..2f1b474afc 100644 --- a/pkg/query-service/app/integrations/test_utils.go +++ b/pkg/query-service/app/integrations/test_utils.go @@ -5,12 +5,12 @@ import ( "slices" "testing" - "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/rules" "go.signoz.io/signoz/pkg/query-service/utils" + "go.signoz.io/signoz/pkg/types" ) func NewTestIntegrationsManager(t *testing.T) *Manager { @@ -92,7 +92,7 @@ func (t *TestAvailableIntegrationsRepo) list( }, }, }, - Dashboards: []dashboards.Data{}, + Dashboards: []types.DashboardData{}, Alerts: []rules.PostableRule{}, }, ConnectionTests: &IntegrationConnectionTests{ @@ -160,7 +160,7 @@ func (t *TestAvailableIntegrationsRepo) list( }, }, }, - Dashboards: []dashboards.Data{}, + Dashboards: []types.DashboardData{}, Alerts: []rules.PostableRule{}, }, ConnectionTests: &IntegrationConnectionTests{ diff --git a/pkg/query-service/app/metricsexplorer/summary.go b/pkg/query-service/app/metricsexplorer/summary.go index bd692c0381..bc63896a8b 100644 --- a/pkg/query-service/app/metricsexplorer/summary.go +++ b/pkg/query-service/app/metricsexplorer/summary.go @@ -15,6 +15,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/model/metrics_explorer" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/rules" + "go.signoz.io/signoz/pkg/types/authtypes" "golang.org/x/sync/errgroup" ) @@ -155,7 +156,11 @@ func (receiver *SummaryService) GetMetricsSummary(ctx context.Context, metricNam g.Go(func() error { var metricNames []string metricNames = append(metricNames, metricName) - data, err := dashboards.GetDashboardsWithMetricNames(ctx, metricNames) + claims, ok := authtypes.ClaimsFromContext(ctx) + if !ok { + return &model.ApiError{Typ: model.ErrorInternal, Err: errors.New("failed to get claims")} + } + data, err := dashboards.GetDashboardsWithMetricNames(ctx, claims.OrgID, metricNames) if err != nil { return err } @@ -322,7 +327,11 @@ func (receiver *SummaryService) GetRelatedMetrics(ctx context.Context, params *m alertsRelatedData := make(map[string][]metrics_explorer.Alert) g.Go(func() error { - names, apiError := dashboards.GetDashboardsWithMetricNames(ctx, metricNames) + claims, ok := authtypes.ClaimsFromContext(ctx) + if !ok { + return &model.ApiError{Typ: model.ErrorInternal, Err: errors.New("failed to get claims")} + } + names, apiError := dashboards.GetDashboardsWithMetricNames(ctx, claims.OrgID, metricNames) if apiError != nil { return apiError } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 977a835867..e768385219 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -101,11 +101,11 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { + if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.BunDB()); err != nil { return nil, err } - if err := explorer.InitWithDSN(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { + if err := explorer.InitWithDSN(serverOptions.SigNoz.SQLStore.BunDB()); err != nil { return nil, err } diff --git a/pkg/query-service/auth/rbac.go b/pkg/query-service/auth/rbac.go index b1aaad1f3e..64d2668170 100644 --- a/pkg/query-service/auth/rbac.go +++ b/pkg/query-service/auth/rbac.go @@ -71,6 +71,8 @@ func IsViewer(user *types.GettableUser) bool { return user.GroupID == AuthCacheO func IsEditor(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.EditorGroupId } func IsAdmin(user *types.GettableUser) bool { return user.GroupID == AuthCacheObj.AdminGroupId } +func IsAdminV2(claims authtypes.Claims) bool { return claims.GroupID == AuthCacheObj.AdminGroupId } + func ValidatePassword(password string) error { if len(password) < minimumPasswordLength { return errors.Errorf("Password should be atleast %d characters.", minimumPasswordLength) diff --git a/pkg/query-service/common/user.go b/pkg/query-service/common/user.go deleted file mode 100644 index a7ce56f9af..0000000000 --- a/pkg/query-service/common/user.go +++ /dev/null @@ -1,16 +0,0 @@ -package common - -import ( - "context" - - "go.signoz.io/signoz/pkg/query-service/constants" - "go.signoz.io/signoz/pkg/types" -) - -func GetUserFromContext(ctx context.Context) *types.GettableUser { - user, ok := ctx.Value(constants.ContextUserKey).(*types.GettableUser) - if !ok { - return nil - } - return user -} diff --git a/pkg/query-service/tests/integration/signoz_integrations_test.go b/pkg/query-service/tests/integration/signoz_integrations_test.go index 9c021836d1..46c7632606 100644 --- a/pkg/query-service/tests/integration/signoz_integrations_test.go +++ b/pkg/query-service/tests/integration/signoz_integrations_test.go @@ -13,7 +13,6 @@ import ( "go.signoz.io/signoz/pkg/http/middleware" "go.signoz.io/signoz/pkg/query-service/app" "go.signoz.io/signoz/pkg/query-service/app/cloudintegrations" - "go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/integrations" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/auth" @@ -350,7 +349,7 @@ func TestDashboardsForInstalledIntegrationDashboards(t *testing.T) { require.GreaterOrEqual(dashboards[0].UpdatedAt.Unix(), tsBeforeInstallation) // Should be able to get installed integrations dashboard by id - dd := integrationsTB.GetDashboardByIdFromQS(dashboards[0].Uuid) + dd := integrationsTB.GetDashboardByIdFromQS(dashboards[0].UUID) require.GreaterOrEqual(dd.CreatedAt.Unix(), tsBeforeInstallation) require.GreaterOrEqual(dd.UpdatedAt.Unix(), tsBeforeInstallation) require.Equal(*dd, dashboards[0]) @@ -464,7 +463,7 @@ func (tb *IntegrationsTestBed) RequestQSToUninstallIntegration( tb.RequestQS("/api/v1/integrations/uninstall", request) } -func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboards.Dashboard { +func (tb *IntegrationsTestBed) GetDashboardsFromQS() []types.Dashboard { result := tb.RequestQS("/api/v1/dashboards", nil) dataJson, err := json.Marshal(result.Data) @@ -472,7 +471,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboards.Dashboard { tb.t.Fatalf("could not marshal apiResponse.Data: %v", err) } - dashboards := []dashboards.Dashboard{} + dashboards := []types.Dashboard{} err = json.Unmarshal(dataJson, &dashboards) if err != nil { tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards") @@ -481,7 +480,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboards.Dashboard { return dashboards } -func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *dashboards.Dashboard { +func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *types.Dashboard { result := tb.RequestQS(fmt.Sprintf("/api/v1/dashboards/%s", dashboardUuid), nil) dataJson, err := json.Marshal(result.Data) @@ -489,7 +488,7 @@ func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *das tb.t.Fatalf("could not marshal apiResponse.Data: %v", err) } - dashboard := dashboards.Dashboard{} + dashboard := types.Dashboard{} err = json.Unmarshal(dataJson, &dashboard) if err != nil { tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards") diff --git a/pkg/query-service/utils/testutils.go b/pkg/query-service/utils/testutils.go index fd0678c6a3..99e90cf35e 100644 --- a/pkg/query-service/utils/testutils.go +++ b/pkg/query-service/utils/testutils.go @@ -48,6 +48,7 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s sqlmigration.NewModifyDatetimeFactory(), sqlmigration.NewModifyOrgDomainFactory(), sqlmigration.NewUpdateOrganizationFactory(sqlStore), + sqlmigration.NewUpdateDashboardAndSavedViewsFactory(sqlStore), ), ) if err != nil { @@ -69,7 +70,7 @@ func NewQueryServiceDBForTests(t *testing.T) sqlstore.SQLStore { if err != nil { t.Fatalf("could not initialize dao: %v", err) } - dashboards.InitDB(sqlStore.SQLxDB()) + dashboards.InitDB(sqlStore.BunDB()) return sqlStore } diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 1568b31204..ea463fdc66 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -59,6 +59,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM sqlmigration.NewModifyOrgDomainFactory(), sqlmigration.NewUpdateOrganizationFactory(sqlstore), sqlmigration.NewAddAlertmanagerFactory(), + sqlmigration.NewUpdateDashboardAndSavedViewsFactory(sqlstore), ) } diff --git a/pkg/sqlmigration/015_update_dashboards_savedviews.go b/pkg/sqlmigration/015_update_dashboards_savedviews.go new file mode 100644 index 0000000000..9ca6d9da57 --- /dev/null +++ b/pkg/sqlmigration/015_update_dashboards_savedviews.go @@ -0,0 +1,80 @@ +package sqlmigration + +import ( + "context" + + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" + "go.signoz.io/signoz/pkg/factory" + "go.signoz.io/signoz/pkg/sqlstore" + "go.signoz.io/signoz/pkg/types" +) + +type updateDashboardAndSavedViews struct { + store sqlstore.SQLStore +} + +func NewUpdateDashboardAndSavedViewsFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("update_group"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return newUpdateDashboardAndSavedViews(ctx, ps, c, sqlstore) + }) +} + +func newUpdateDashboardAndSavedViews(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) { + return &updateDashboardAndSavedViews{ + store: store, + }, nil +} + +func (migration *updateDashboardAndSavedViews) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *updateDashboardAndSavedViews) Up(ctx context.Context, db *bun.DB) error { + + // begin transaction + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // get all org ids + var orgIDs []string + if err := migration.store.BunDB().NewSelect().Model((*types.Organization)(nil)).Column("id").Scan(ctx, &orgIDs); err != nil { + return err + } + + // add org id to dashboards table + for _, table := range []string{"dashboards", "saved_views"} { + if exists, err := migration.store.Dialect().ColumnExists(ctx, tx, table, "org_id"); err != nil { + return err + } else if !exists { + if _, err := tx.NewAddColumn().Table(table).ColumnExpr("org_id TEXT REFERENCES organizations(id) ON DELETE CASCADE").Exec(ctx); err != nil { + return err + } + + // check if there is one org ID if yes then set it to all dashboards. + if len(orgIDs) == 1 { + orgID := orgIDs[0] + if _, err := tx.NewUpdate().Table(table).Set("org_id = ?", orgID).Where("org_id IS NULL").Exec(ctx); err != nil { + return err + } + } + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (migration *updateDashboardAndSavedViews) Down(ctx context.Context, db *bun.DB) error { + return nil +} diff --git a/pkg/types/dashboard.go b/pkg/types/dashboard.go index aeafb9a2c7..686a3c5a56 100644 --- a/pkg/types/dashboard.go +++ b/pkg/types/dashboard.go @@ -1,22 +1,71 @@ package types import ( + "database/sql/driver" + "encoding/base64" + "encoding/json" + "strings" "time" + "github.com/gosimple/slug" "github.com/uptrace/bun" ) type Dashboard struct { bun.BaseModel `bun:"table:dashboards"` - ID int `bun:"id,pk,autoincrement"` - UUID string `bun:"uuid,type:text,notnull,unique"` - CreatedAt time.Time `bun:"created_at,type:datetime,notnull"` - CreatedBy string `bun:"created_by,type:text,notnull"` - UpdatedAt time.Time `bun:"updated_at,type:datetime,notnull"` - UpdatedBy string `bun:"updated_by,type:text,notnull"` - Data string `bun:"data,type:text,notnull"` - Locked int `bun:"locked,notnull,default:0"` + TimeAuditable + UserAuditable + OrgID string `json:"-" bun:"org_id,notnull"` + ID int `json:"id" bun:"id,pk,autoincrement"` + UUID string `json:"uuid" bun:"uuid,type:text,notnull,unique"` + Data DashboardData `json:"data" bun:"data,type:text,notnull"` + Locked *int `json:"isLocked" bun:"locked,notnull,default:0"` + + Slug string `json:"-" bun:"-"` + Title string `json:"-" bun:"-"` +} + +// UpdateSlug updates the slug +func (d *Dashboard) UpdateSlug() { + var title string + + if val, ok := d.Data["title"]; ok { + title = val.(string) + } + + d.Slug = SlugifyTitle(title) +} + +func SlugifyTitle(title string) string { + s := slug.Make(strings.ToLower(title)) + if s == "" { + // If the dashboard name is only characters outside of the + // sluggable characters, the slug creation will return an + // empty string which will mess up URLs. This failsafe picks + // that up and creates the slug as a base64 identifier instead. + s = base64.RawURLEncoding.EncodeToString([]byte(title)) + if slug.MaxLength != 0 && len(s) > slug.MaxLength { + s = s[:slug.MaxLength] + } + } + return s +} + +type DashboardData map[string]interface{} + +func (c DashboardData) Value() (driver.Value, error) { + return json.Marshal(c) +} + +func (c *DashboardData) Scan(src interface{}) error { + var data []byte + if b, ok := src.([]byte); ok { + data = b + } else if s, ok := src.(string); ok { + data = []byte(s) + } + return json.Unmarshal(data, c) } type Rule struct { diff --git a/pkg/types/savedview.go b/pkg/types/savedview.go index 8ec958ec27..3ebc2d9d70 100644 --- a/pkg/types/savedview.go +++ b/pkg/types/savedview.go @@ -1,23 +1,20 @@ package types import ( - "time" - "github.com/uptrace/bun" ) type SavedView struct { bun.BaseModel `bun:"table:saved_views"` - UUID string `bun:"uuid,pk,type:text"` - Name string `bun:"name,type:text,notnull"` - Category string `bun:"category,type:text,notnull"` - CreatedAt time.Time `bun:"created_at,type:datetime,notnull"` - CreatedBy string `bun:"created_by,type:text"` - UpdatedAt time.Time `bun:"updated_at,type:datetime,notnull"` - UpdatedBy string `bun:"updated_by,type:text"` - SourcePage string `bun:"source_page,type:text,notnull"` - Tags string `bun:"tags,type:text"` - Data string `bun:"data,type:text,notnull"` - ExtraData string `bun:"extra_data,type:text"` + TimeAuditable + UserAuditable + OrgID string `json:"orgId" bun:"org_id,notnull"` + UUID string `json:"uuid" bun:"uuid,pk,type:text"` + Name string `json:"name" bun:"name,type:text,notnull"` + Category string `json:"category" bun:"category,type:text,notnull"` + SourcePage string `json:"sourcePage" bun:"source_page,type:text,notnull"` + Tags string `json:"tags" bun:"tags,type:text"` + Data string `json:"data" bun:"data,type:text,notnull"` + ExtraData string `json:"extraData" bun:"extra_data,type:text"` }