mirror of
https://github.com/grafana/grafana.git
synced 2024-12-02 05:29:42 -06:00
d076bedb5e
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>
471 lines
11 KiB
Go
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
|
|
}
|