grafana/pkg/models/dashboards.go
Jeff Levin d076bedb5e
public dashboards: finalize db schema & v1 feature complete (#50467)
This PR completes public dashboards v1 functionality and simplifies public dashboard conventions. It exists as a large PR so that we are not making constant changes to the database schema.

models.PublicDashboardConfig model replaced with models.PublicDashboard directly
dashboard_public_config table renamed to dashboard_public
models.Dashboard.IsPublic removed from the dashboard and replaced with models.PublicDashboard.isEnabled
Routing now uses a uuid v4 as an access token for viewing a public dashboard anonymously, PublicDashboard.Uid only used as database identifier
Frontend utilizes uuid for auth'd operations and access token for anonymous access
Default to time range defined on dashboard when viewing public dashboard
Add audit fields to public dashboard

Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>, Ezequiel Victorero <ezequiel.victorero@grafana.com>, Jesse Weaver <jesse.weaver@grafana.com>
2022-06-22 13:58:52 -08:00

471 lines
11 KiB
Go

package models
import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const RootFolderName = "General"
// Typed errors
var (
ErrDashboardNotFound = DashboardErr{
Reason: "Dashboard not found",
StatusCode: 404,
Status: "not-found",
}
ErrDashboardCorrupt = DashboardErr{
Reason: "Dashboard data is missing or corrupt",
StatusCode: 500,
Status: "not-found",
}
ErrDashboardPanelNotFound = DashboardErr{
Reason: "Dashboard panel not found",
StatusCode: 404,
Status: "not-found",
}
ErrDashboardFolderNotFound = DashboardErr{
Reason: "Folder not found",
StatusCode: 404,
}
ErrDashboardSnapshotNotFound = DashboardErr{
Reason: "Dashboard snapshot not found",
StatusCode: 404,
}
ErrDashboardWithSameUIDExists = DashboardErr{
Reason: "A dashboard with the same uid already exists",
StatusCode: 400,
}
ErrDashboardWithSameNameInFolderExists = DashboardErr{
Reason: "A dashboard with the same name in the folder already exists",
StatusCode: 412,
Status: "name-exists",
}
ErrDashboardVersionMismatch = DashboardErr{
Reason: "The dashboard has been changed by someone else",
StatusCode: 412,
Status: "version-mismatch",
}
ErrDashboardTitleEmpty = DashboardErr{
Reason: "Dashboard title cannot be empty",
StatusCode: 400,
Status: "empty-name",
}
ErrDashboardFolderCannotHaveParent = DashboardErr{
Reason: "A Dashboard Folder cannot be added to another folder",
StatusCode: 400,
}
ErrDashboardsWithSameSlugExists = DashboardErr{
Reason: "Multiple dashboards with the same slug exists",
StatusCode: 412,
}
ErrDashboardFailedGenerateUniqueUid = DashboardErr{
Reason: "Failed to generate unique dashboard id",
StatusCode: 500,
}
ErrDashboardTypeMismatch = DashboardErr{
Reason: "Dashboard cannot be changed to a folder",
StatusCode: 400,
}
ErrDashboardFolderWithSameNameAsDashboard = DashboardErr{
Reason: "Folder name cannot be the same as one of its dashboards",
StatusCode: 400,
}
ErrDashboardWithSameNameAsFolder = DashboardErr{
Reason: "Dashboard name cannot be the same as folder",
StatusCode: 400,
Status: "name-match",
}
ErrDashboardFolderNameExists = DashboardErr{
Reason: "A folder with that name already exists",
StatusCode: 400,
}
ErrDashboardUpdateAccessDenied = DashboardErr{
Reason: "Access denied to save dashboard",
StatusCode: 403,
}
ErrDashboardInvalidUid = DashboardErr{
Reason: "uid contains illegal characters",
StatusCode: 400,
}
ErrDashboardUidTooLong = DashboardErr{
Reason: "uid too long, max 40 characters",
StatusCode: 400,
}
ErrDashboardCannotSaveProvisionedDashboard = DashboardErr{
Reason: "Cannot save provisioned dashboard",
StatusCode: 400,
}
ErrDashboardRefreshIntervalTooShort = DashboardErr{
Reason: "Dashboard refresh interval is too low",
StatusCode: 400,
}
ErrDashboardCannotDeleteProvisionedDashboard = DashboardErr{
Reason: "provisioned dashboard cannot be deleted",
StatusCode: 400,
}
ErrDashboardIdentifierNotSet = DashboardErr{
Reason: "Unique identifier needed to be able to get a dashboard",
StatusCode: 400,
}
ErrDashboardIdentifierInvalid = DashboardErr{
Reason: "Dashboard ID not a number",
StatusCode: 400,
}
ErrDashboardPanelIdentifierInvalid = DashboardErr{
Reason: "Dashboard panel ID not a number",
StatusCode: 400,
}
ErrDashboardOrPanelIdentifierNotSet = DashboardErr{
Reason: "Unique identifier needed to be able to get a dashboard panel",
StatusCode: 400,
}
ErrProvisionedDashboardNotFound = DashboardErr{
Reason: "Dashboard is not provisioned",
StatusCode: 404,
Status: "not-found",
}
ErrDashboardThumbnailNotFound = DashboardErr{
Reason: "Dashboard thumbnail not found",
StatusCode: 404,
Status: "not-found",
}
)
// DashboardErr represents a dashboard error.
type DashboardErr struct {
StatusCode int
Status string
Reason string
}
// Equal returns whether equal to another DashboardErr.
func (e DashboardErr) Equal(o DashboardErr) bool {
return o.StatusCode == e.StatusCode && o.Status == e.Status && o.Reason == e.Reason
}
// Error returns the error message.
func (e DashboardErr) Error() string {
if e.Reason != "" {
return e.Reason
}
return "Dashboard Error"
}
// Body returns the error's response body, if applicable.
func (e DashboardErr) Body() util.DynMap {
if e.Status == "" {
return nil
}
return util.DynMap{"status": e.Status, "message": e.Error()}
}
type UpdatePluginDashboardError struct {
PluginId string
}
func (d UpdatePluginDashboardError) Error() string {
return "Dashboard belongs to plugin"
}
const (
DashTypeDB = "db"
DashTypeSnapshot = "snapshot"
)
// Dashboard model
type Dashboard struct {
Id int64
Uid string
Slug string
OrgId int64
GnetId int64
Version int
PluginId string
Created time.Time
Updated time.Time
UpdatedBy int64
CreatedBy int64
FolderId int64
IsFolder bool
HasAcl bool
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)
}
// GetDashboardIdForSavePermissionCheck return the dashboard id to be used for checking permission of dashboard
func (d *Dashboard) GetDashboardIdForSavePermissionCheck() int64 {
if d.Id == 0 {
return d.FolderId
}
return d.Id
}
// 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.UpdateSlug()
return dash
}
// UpdateSlug updates the slug
func (d *Dashboard) UpdateSlug() {
title := d.Data.Get("title").MustString()
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
}
// 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 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"`
Overwrite bool `json:"overwrite"`
Message string `json:"message"`
OrgId int64 `json:"-"`
RestoredFrom int `json:"-"`
PluginId string `json:"-"`
FolderId int64 `json:"folderId"`
FolderUid string `json:"folderUid"`
IsFolder bool `json:"isFolder"`
UpdatedAt time.Time
Result *Dashboard `json:"-"`
}
type TrimDashboardCommand struct {
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
Meta *simplejson.Json `json:"meta"`
Result *Dashboard `json:"-"`
}
type DashboardProvisioning struct {
Id int64
DashboardId int64
Name string
ExternalId string
CheckSum string
Updated int64
}
type DeleteDashboardCommand struct {
Id int64
OrgId int64
ForceDeleteFolderRules bool
}
type DeleteOrphanedProvisionedDashboardsCommand struct {
ReaderNames []string
}
//
// QUERIES
//
type GetDashboardQuery struct {
Slug string // required if no Id or Uid is specified
Id int64 // optional if slug is set
Uid string // optional if slug is set
OrgId int64
Result *Dashboard
}
type DashboardTagCloudItem struct {
Term string `json:"term"`
Count int `json:"count"`
}
type GetDashboardTagsQuery struct {
OrgId int64
Result []*DashboardTagCloudItem
}
type GetDashboardsQuery struct {
DashboardIds []int64
DashboardUIds []string
Result []*Dashboard
}
type GetDashboardsByPluginIdQuery struct {
OrgId int64
PluginId string
Result []*Dashboard
}
type GetDashboardSlugByIdQuery struct {
Id int64
Result string
}
type GetDashboardsBySlugQuery struct {
OrgId int64
Slug string
Result []*Dashboard
}
type DashboardRef struct {
Uid string
Slug string
}
type GetDashboardRefByIdQuery struct {
Id int64
Result *DashboardRef
}