grafana/pkg/services/dashboards/models.go
Sofia Papagiannaki 03a626f1d6
Search: Fix empty folder details for nested folder items (#76504)
* Introduce dashboard.folder_uid column

* Add data migration

* Search: Fix empty folder details for nested folders

* Set `dashboard.folder_uid` and update tests

* Add unique index

* lint

Ignore cyclomatic complexity of func
`(*DashboardServiceImpl).BuildSaveDashboardCommand

* Fix search by folder UID
2023-10-24 10:04:45 +03:00

496 lines
13 KiB
Go

package dashboards
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/kinds"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const RootFolderName = "General"
const (
DashTypeDB = "db"
DashTypeSnapshot = "snapshot"
)
// Dashboard model
type Dashboard struct {
ID int64 `xorm:"pk autoincr 'id'"`
UID string `xorm:"uid"`
Slug string
OrgID int64 `xorm:"org_id"`
GnetID int64 `xorm:"gnet_id"`
Version int
PluginID string `xorm:"plugin_id"`
Created time.Time
Updated time.Time
UpdatedBy int64
CreatedBy int64
FolderID int64 `xorm:"folder_id"`
FolderUID string `xorm:"folder_uid"`
IsFolder bool
HasACL bool `xorm:"has_acl"`
Title string
Data *simplejson.Json
}
func (d *Dashboard) SetID(id int64) {
d.ID = id
d.Data.Set("id", id)
}
func (d *Dashboard) SetUID(uid string) {
d.UID = uid
d.Data.Set("uid", uid)
}
func (d *Dashboard) SetVersion(version int) {
d.Version = version
d.Data.Set("version", version)
}
func (d *Dashboard) ToResource() kinds.GrafanaResource[simplejson.Json, any] {
parent := dashboard.NewK8sResource(d.UID, nil)
res := kinds.GrafanaResource[simplejson.Json, any]{
Kind: parent.Kind,
APIVersion: parent.APIVersion,
Metadata: kinds.GrafanaResourceMetadata{
Name: d.UID,
Annotations: make(map[string]string),
Labels: make(map[string]string),
CreationTimestamp: v1.NewTime(d.Created),
ResourceVersion: fmt.Sprintf("%d", d.Version),
},
}
if d.Data != nil {
copy := &simplejson.Json{}
db, _ := d.Data.ToDB()
_ = copy.FromDB(db)
copy.Del("id")
copy.Del("version") // ???
copy.Del("uid") // duplicated to name
res.Spec = copy
}
d.UpdateSlug()
res.Metadata.SetUpdatedTimestamp(&d.Updated)
res.Metadata.SetSlug(d.Slug)
if d.CreatedBy > 0 {
res.Metadata.SetCreatedBy(fmt.Sprintf("user:%d", d.CreatedBy))
}
if d.UpdatedBy > 0 {
res.Metadata.SetUpdatedBy(fmt.Sprintf("user:%d", d.UpdatedBy))
}
if d.PluginID != "" {
res.Metadata.SetOriginInfo(&kinds.ResourceOriginInfo{
Name: "plugin",
Key: d.PluginID,
})
}
if d.FolderID > 0 {
res.Metadata.SetFolder(fmt.Sprintf("folder:%d", d.FolderID))
}
if d.IsFolder {
res.Kind = "Folder"
}
return res
}
// NewDashboard creates a new dashboard
func NewDashboard(title string) *Dashboard {
dash := &Dashboard{}
dash.Data = simplejson.New()
dash.Data.Set("title", title)
dash.Title = title
dash.Created = time.Now()
dash.Updated = time.Now()
dash.UpdateSlug()
return dash
}
// NewDashboardFolder creates a new dashboard folder
func NewDashboardFolder(title string) *Dashboard {
folder := NewDashboard(title)
folder.IsFolder = true
folder.Data.Set("schemaVersion", 17)
folder.Data.Set("version", 0)
folder.IsFolder = true
return folder
}
// GetTags turns the tags in data json into go string array
func (d *Dashboard) GetTags() []string {
return d.Data.Get("tags").MustStringArray()
}
func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash := &Dashboard{}
dash.Data = data
dash.Title = dash.Data.Get("title").MustString()
dash.UpdateSlug()
update := false
if id, err := dash.Data.Get("id").Float64(); err == nil {
dash.ID = int64(id)
update = true
}
if uid, err := dash.Data.Get("uid").String(); err == nil {
dash.UID = uid
update = true
}
if version, err := dash.Data.Get("version").Float64(); err == nil && update {
dash.Version = int(version)
dash.Updated = time.Now()
} else {
dash.Data.Set("version", 0)
dash.Created = time.Now()
dash.Updated = time.Now()
}
if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil {
dash.GnetID = int64(gnetId)
}
return dash
}
// GetDashboardModel turns the command into the saveable model
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash := NewDashboardFromJson(cmd.Dashboard)
userID := cmd.UserID
if userID == 0 {
userID = -1
}
dash.UpdatedBy = userID
dash.OrgID = cmd.OrgID
dash.PluginID = cmd.PluginID
dash.IsFolder = cmd.IsFolder
dash.FolderID = cmd.FolderID
dash.FolderUID = cmd.FolderUID
dash.UpdateSlug()
return dash
}
// UpdateSlug updates the slug
func (d *Dashboard) UpdateSlug() {
title := d.Data.Get("title").MustString()
d.Slug = slugify.Slugify(title)
}
// GetURL return the html url for a folder if it's folder, otherwise for a dashboard
func (d *Dashboard) GetURL() string {
return GetDashboardFolderURL(d.IsFolder, d.UID, d.Slug)
}
// GetDashboardFolderURL return the html url for a folder if it's folder, otherwise for a dashboard
func GetDashboardFolderURL(isFolder bool, uid string, slug string) string {
if isFolder {
return GetFolderURL(uid, slug)
}
return GetDashboardURL(uid, slug)
}
// GetDashboardURL returns the HTML url for a dashboard.
func GetDashboardURL(uid string, slug string) string {
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
}
// GetKioskModeDashboardUrl returns the HTML url for a dashboard in kiosk mode.
func GetKioskModeDashboardURL(uid string, slug string, theme models.Theme) string {
return fmt.Sprintf("%s?kiosk&theme=%s", GetDashboardURL(uid, slug), string(theme))
}
// GetFullDashboardURL returns the full URL for a dashboard.
func GetFullDashboardURL(uid string, slug string) string {
return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug)
}
// GetFolderURL returns the HTML url for a folder.
func GetFolderURL(folderUID string, slug string) string {
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUID, slug)
}
type ValidateDashboardBeforeSaveResult struct {
IsParentFolderChanged bool
}
//
// COMMANDS
//
type SaveDashboardCommand struct {
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
UserID int64 `json:"userId" xorm:"user_id"`
Overwrite bool `json:"overwrite"`
Message string `json:"message"`
OrgID int64 `json:"-" xorm:"org_id"`
RestoredFrom int `json:"-"`
PluginID string `json:"-" xorm:"plugin_id"`
// Deprecated: use FolderUID instead
FolderID int64 `json:"folderId" xorm:"folder_id"`
FolderUID string `json:"folderUid" xorm:"folder_uid"`
IsFolder bool `json:"isFolder"`
UpdatedAt time.Time
}
type ValidateDashboardCommand struct {
Dashboard string `json:"dashboard" binding:"Required"`
}
type TrimDashboardCommand struct {
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
Meta *simplejson.Json `json:"meta"`
}
type DashboardProvisioning struct {
ID int64 `xorm:"pk autoincr 'id'"`
DashboardID int64 `xorm:"dashboard_id"`
Name string
ExternalID string `xorm:"external_id"`
CheckSum string
Updated int64
}
type DeleteDashboardCommand struct {
ID int64
OrgID int64
ForceDeleteFolderRules bool
}
type DeleteOrphanedProvisionedDashboardsCommand struct {
ReaderNames []string
}
//
// QUERIES
//
// GetDashboardQuery is used to query for a single dashboard matching
// a unique constraint within the provided OrgID.
//
// Available constraints:
// - ID uses Grafana's internal numeric database identifier to get a
// dashboard.
// - UID use the unique identifier to get a dashboard.
// - Title + FolderID uses the combination of the dashboard's
// human-readable title and its parent folder's ID
// (or zero, for top level items). Both are required if no other
// constraint is set.
//
// Multiple constraints can be combined.
type GetDashboardQuery struct {
ID int64
UID string
Title *string
FolderID *int64
OrgID int64
}
type DashboardTagCloudItem struct {
Term string `json:"term"`
Count int `json:"count"`
}
type GetDashboardTagsQuery struct {
OrgID int64
}
type GetDashboardsQuery struct {
DashboardIDs []int64
DashboardUIDs []string
OrgID int64
}
type GetDashboardsByPluginIDQuery struct {
OrgID int64
PluginID string
}
type DashboardRef struct {
UID string `xorm:"uid"`
Slug string
}
type GetDashboardRefByIDQuery struct {
ID int64
}
type SaveDashboardDTO struct {
OrgID int64
UpdatedAt time.Time
User identity.Requester
Message string
Overwrite bool
Dashboard *Dashboard
}
type DashboardSearchProjection struct {
ID int64 `xorm:"id"`
UID string `xorm:"uid"`
Title string
Slug string
Term string
IsFolder bool
FolderID int64 `xorm:"folder_id"`
FolderUID string `xorm:"folder_uid"`
FolderSlug string
FolderTitle string
SortMeta int64
}
const (
QuotaTargetSrv quota.TargetSrv = "dashboard"
QuotaTarget quota.Target = "dashboard"
)
type CountDashboardsInFolderQuery struct {
FolderUID string
OrgID int64
}
// TODO: CountDashboardsInFolderRequest is the request passed from the service
// to the store layer. The FolderID will be replaced with FolderUID when
// dashboards are updated with parent folder UIDs.
type CountDashboardsInFolderRequest struct {
FolderID int64
OrgID int64
}
func FromDashboard(dash *Dashboard) *folder.Folder {
return &folder.Folder{
ID: dash.ID,
UID: dash.UID,
OrgID: dash.OrgID,
Title: dash.Title,
HasACL: dash.HasACL,
URL: GetFolderURL(dash.UID, dash.Slug),
Version: dash.Version,
Created: dash.Created,
CreatedBy: dash.CreatedBy,
Updated: dash.Updated,
UpdatedBy: dash.UpdatedBy,
}
}
type DeleteDashboardsInFolderRequest struct {
FolderUID string
OrgID int64
}
//
// DASHBOARD ACL
//
// Dashboard ACL model
type DashboardACL struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
DashboardID int64 `xorm:"dashboard_id"`
UserID int64 `xorm:"user_id"`
TeamID int64 `xorm:"team_id"`
Role *org.RoleType // pointer to be nullable
Permission PermissionType
Created time.Time
Updated time.Time
}
func (p DashboardACL) TableName() string { return "dashboard_acl" }
type DashboardACLInfoDTO struct {
OrgID int64 `json:"-" xorm:"org_id"`
DashboardID int64 `json:"dashboardId,omitempty" xorm:"dashboard_id"`
FolderID int64 `json:"folderId,omitempty" xorm:"folder_id"`
FolderUID string `json:"folderUid,omitempty" xorm:"folder_uid"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UserID int64 `json:"userId" xorm:"user_id"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
UserAvatarURL string `json:"userAvatarUrl" xorm:"user_avatar_url"`
TeamID int64 `json:"teamId" xorm:"team_id"`
TeamEmail string `json:"teamEmail"`
TeamAvatarURL string `json:"teamAvatarUrl" xorm:"team_avatar_url"`
Team string `json:"team"`
Role *org.RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
PermissionName string `json:"permissionName"`
UID string `json:"uid" xorm:"uid"`
Title string `json:"title"`
Slug string `json:"slug"`
IsFolder bool `json:"isFolder"`
URL string `json:"url" xorm:"url"`
Inherited bool `json:"inherited"`
}
func (dto *DashboardACLInfoDTO) hasSameRoleAs(other *DashboardACLInfoDTO) bool {
if dto.Role == nil || other.Role == nil {
return false
}
return dto.UserID <= 0 && dto.TeamID <= 0 && dto.UserID == other.UserID && dto.TeamID == other.TeamID && *dto.Role == *other.Role
}
func (dto *DashboardACLInfoDTO) hasSameUserAs(other *DashboardACLInfoDTO) bool {
return dto.UserID > 0 && dto.UserID == other.UserID
}
func (dto *DashboardACLInfoDTO) hasSameTeamAs(other *DashboardACLInfoDTO) bool {
return dto.TeamID > 0 && dto.TeamID == other.TeamID
}
// IsDuplicateOf returns true if other item has same role, same user or same team
func (dto *DashboardACLInfoDTO) IsDuplicateOf(other *DashboardACLInfoDTO) bool {
return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other)
}
// QUERIES
type GetDashboardACLInfoListQuery struct {
DashboardID int64
OrgID int64
}
type FindPersistedDashboardsQuery struct {
Title string
OrgId int64
SignedInUser *user.SignedInUser
DashboardIds []int64
DashboardUIDs []string
Type string
FolderIds []int64
FolderUIDs []string
Tags []string
Limit int64
Page int64
Permission PermissionType
Sort model.SortOption
Filters []any
}