mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 11:02:49 +08:00

This feature would provide a way for administrators to limit the minimum dashboard refresh interval globally. Filters out the refresh intervals available in the time picker that are lower than the set minimum refresh interval in the configuration .ini file Adds the minimum refresh interval as available in the time picker. If the user tries to enter a refresh interval that is lower than the minimum in the URL, defaults to the minimum interval. When trying to update the JSON via the API, rejects the update if the dashboard's refresh interval is lower than the minimum. When trying to update a dashboard via provisioning having a lower refresh interval than the minimum, defaults to the minimum interval and logs a warning. Fixes #3356 Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
542 lines
16 KiB
Go
542 lines
16 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
|
|
"github.com/grafana/grafana/pkg/services/alerting"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/components/dashdiffs"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
|
m "github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/services/guardian"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
const (
|
|
anonString = "Anonymous"
|
|
)
|
|
|
|
func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) {
|
|
if !c.IsSignedIn {
|
|
return false, nil
|
|
}
|
|
|
|
query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashID}
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return query.Result, nil
|
|
}
|
|
|
|
func dashboardGuardianResponse(err error) Response {
|
|
if err != nil {
|
|
return Error(500, "Error while checking dashboard permissions", err)
|
|
}
|
|
|
|
return Error(403, "Access denied to this dashboard", nil)
|
|
}
|
|
|
|
func (hs *HTTPServer) GetDashboard(c *m.ReqContext) Response {
|
|
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
|
|
if rsp != nil {
|
|
return rsp
|
|
}
|
|
|
|
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
|
|
if canView, err := guardian.CanView(); err != nil || !canView {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
canEdit, _ := guardian.CanEdit()
|
|
canSave, _ := guardian.CanSave()
|
|
canAdmin, _ := guardian.CanAdmin()
|
|
|
|
isStarred, err := isDashboardStarredByUser(c, dash.Id)
|
|
if err != nil {
|
|
return Error(500, "Error while checking if dashboard was starred by user", err)
|
|
}
|
|
|
|
// Finding creator and last updater of the dashboard
|
|
updater, creator := anonString, anonString
|
|
if dash.UpdatedBy > 0 {
|
|
updater = getUserLogin(dash.UpdatedBy)
|
|
}
|
|
if dash.CreatedBy > 0 {
|
|
creator = getUserLogin(dash.CreatedBy)
|
|
}
|
|
|
|
meta := dtos.DashboardMeta{
|
|
IsStarred: isStarred,
|
|
Slug: dash.Slug,
|
|
Type: m.DashTypeDB,
|
|
CanStar: c.IsSignedIn,
|
|
CanSave: canSave,
|
|
CanEdit: canEdit,
|
|
CanAdmin: canAdmin,
|
|
Created: dash.Created,
|
|
Updated: dash.Updated,
|
|
UpdatedBy: updater,
|
|
CreatedBy: creator,
|
|
Version: dash.Version,
|
|
HasAcl: dash.HasAcl,
|
|
IsFolder: dash.IsFolder,
|
|
FolderId: dash.FolderId,
|
|
Url: dash.GetUrl(),
|
|
FolderTitle: "General",
|
|
}
|
|
|
|
// lookup folder title
|
|
if dash.FolderId > 0 {
|
|
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
return Error(500, "Dashboard folder could not be read", err)
|
|
}
|
|
meta.FolderTitle = query.Result.Title
|
|
meta.FolderUrl = query.Result.GetUrl()
|
|
}
|
|
|
|
provisioningData, err := dashboards.NewProvisioningService().GetProvisionedDashboardDataByDashboardId(dash.Id)
|
|
if err != nil {
|
|
return Error(500, "Error while checking if dashboard is provisioned", err)
|
|
}
|
|
|
|
if provisioningData != nil {
|
|
allowUiUpdate := hs.ProvisioningService.GetAllowUiUpdatesFromConfig(provisioningData.Name)
|
|
if !allowUiUpdate {
|
|
meta.Provisioned = true
|
|
}
|
|
|
|
meta.ProvisionedExternalId, err = filepath.Rel(
|
|
hs.ProvisioningService.GetDashboardProvisionerResolvedPath(provisioningData.Name),
|
|
provisioningData.ExternalId,
|
|
)
|
|
if err != nil {
|
|
// Not sure when this could happen so not sure how to better handle this. Right now ProvisionedExternalId
|
|
// is for better UX, showing in Save/Delete dialogs and so it won't break anything if it is empty.
|
|
hs.log.Warn("Failed to create ProvisionedExternalId", "err", err)
|
|
}
|
|
}
|
|
|
|
// make sure db version is in sync with json model version
|
|
dash.Data.Set("version", dash.Version)
|
|
|
|
dto := dtos.DashboardFullWithMeta{
|
|
Dashboard: dash.Data,
|
|
Meta: meta,
|
|
}
|
|
|
|
c.TimeRequest(metrics.MApiDashboardGet)
|
|
return JSON(200, dto)
|
|
}
|
|
|
|
func getUserLogin(userID int64) string {
|
|
query := m.GetUserByIdQuery{Id: userID}
|
|
err := bus.Dispatch(&query)
|
|
if err != nil {
|
|
return anonString
|
|
}
|
|
return query.Result.Login
|
|
}
|
|
|
|
func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dashboard, Response) {
|
|
var query m.GetDashboardQuery
|
|
|
|
if len(uid) > 0 {
|
|
query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID}
|
|
} else {
|
|
query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgID}
|
|
}
|
|
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
return nil, Error(404, "Dashboard not found", err)
|
|
}
|
|
|
|
return query.Result, nil
|
|
}
|
|
|
|
func DeleteDashboardBySlug(c *m.ReqContext) Response {
|
|
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
|
|
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
return Error(500, "Failed to retrieve dashboards by slug", err)
|
|
}
|
|
|
|
if len(query.Result) > 1 {
|
|
return JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
|
|
}
|
|
|
|
return deleteDashboard(c)
|
|
}
|
|
|
|
func DeleteDashboardByUID(c *m.ReqContext) Response {
|
|
return deleteDashboard(c)
|
|
}
|
|
|
|
func deleteDashboard(c *m.ReqContext) Response {
|
|
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid"))
|
|
if rsp != nil {
|
|
return rsp
|
|
}
|
|
|
|
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
err := dashboards.NewService().DeleteDashboard(dash.Id, c.OrgId)
|
|
if err == m.ErrDashboardCannotDeleteProvisionedDashboard {
|
|
return Error(400, "Dashboard cannot be deleted because it was provisioned", err)
|
|
} else if err != nil {
|
|
return Error(500, "Failed to delete dashboard", err)
|
|
}
|
|
|
|
return JSON(200, util.DynMap{
|
|
"title": dash.Title,
|
|
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
|
})
|
|
}
|
|
|
|
func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
|
cmd.OrgId = c.OrgId
|
|
cmd.UserId = c.UserId
|
|
|
|
dash := cmd.GetDashboardModel()
|
|
|
|
newDashboard := dash.Id == 0 && dash.Uid == ""
|
|
if newDashboard {
|
|
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
|
if err != nil {
|
|
return Error(500, "failed to get quota", err)
|
|
}
|
|
if limitReached {
|
|
return Error(403, "Quota reached", nil)
|
|
}
|
|
}
|
|
|
|
provisioningData, err := dashboards.NewProvisioningService().GetProvisionedDashboardDataByDashboardId(dash.Id)
|
|
if err != nil {
|
|
return Error(500, "Error while checking if dashboard is provisioned", err)
|
|
}
|
|
|
|
allowUiUpdate := true
|
|
if provisioningData != nil {
|
|
allowUiUpdate = hs.ProvisioningService.GetAllowUiUpdatesFromConfig(provisioningData.Name)
|
|
}
|
|
|
|
dashItem := &dashboards.SaveDashboardDTO{
|
|
Dashboard: dash,
|
|
Message: cmd.Message,
|
|
OrgId: c.OrgId,
|
|
User: c.SignedInUser,
|
|
Overwrite: cmd.Overwrite,
|
|
}
|
|
|
|
dashboard, err := dashboards.NewService().SaveDashboard(dashItem, allowUiUpdate)
|
|
if err != nil {
|
|
return dashboardSaveErrorToApiResponse(err)
|
|
}
|
|
|
|
if hs.Cfg.EditorsCanAdmin && newDashboard {
|
|
inFolder := cmd.FolderId > 0
|
|
err := dashboards.MakeUserAdmin(hs.Bus, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder)
|
|
if err != nil {
|
|
hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err)
|
|
}
|
|
}
|
|
|
|
c.TimeRequest(metrics.MApiDashboardSave)
|
|
return JSON(200, util.DynMap{
|
|
"status": "success",
|
|
"slug": dashboard.Slug,
|
|
"version": dashboard.Version,
|
|
"id": dashboard.Id,
|
|
"uid": dashboard.Uid,
|
|
"url": dashboard.GetUrl(),
|
|
})
|
|
}
|
|
|
|
func dashboardSaveErrorToApiResponse(err error) Response {
|
|
if err == m.ErrDashboardTitleEmpty ||
|
|
err == m.ErrDashboardWithSameNameAsFolder ||
|
|
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
|
|
err == m.ErrDashboardTypeMismatch ||
|
|
err == m.ErrDashboardInvalidUid ||
|
|
err == m.ErrDashboardUidToLong ||
|
|
err == m.ErrDashboardWithSameUIDExists ||
|
|
err == m.ErrFolderNotFound ||
|
|
err == m.ErrDashboardFolderCannotHaveParent ||
|
|
err == m.ErrDashboardFolderNameExists ||
|
|
err == m.ErrDashboardRefreshIntervalTooShort ||
|
|
err == m.ErrDashboardCannotSaveProvisionedDashboard {
|
|
return Error(400, err.Error(), nil)
|
|
}
|
|
|
|
if err == m.ErrDashboardUpdateAccessDenied {
|
|
return Error(403, err.Error(), err)
|
|
}
|
|
|
|
if validationErr, ok := err.(alerting.ValidationError); ok {
|
|
return Error(422, validationErr.Error(), nil)
|
|
}
|
|
|
|
if err == m.ErrDashboardWithSameNameInFolderExists {
|
|
return JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
|
}
|
|
|
|
if err == m.ErrDashboardVersionMismatch {
|
|
return JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
|
}
|
|
|
|
if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
|
|
message := "The dashboard belongs to plugin " + pluginErr.PluginId + "."
|
|
// look up plugin name
|
|
if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
|
|
message = "The dashboard belongs to plugin " + pluginDef.Name + "."
|
|
}
|
|
return JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message})
|
|
}
|
|
|
|
if err == m.ErrDashboardNotFound {
|
|
return JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
|
}
|
|
|
|
return Error(500, "Failed to save dashboard", err)
|
|
}
|
|
|
|
func GetHomeDashboard(c *m.ReqContext) Response {
|
|
prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
|
|
if err := bus.Dispatch(&prefsQuery); err != nil {
|
|
return Error(500, "Failed to get preferences", err)
|
|
}
|
|
|
|
if prefsQuery.Result.HomeDashboardId != 0 {
|
|
slugQuery := m.GetDashboardRefByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
|
|
err := bus.Dispatch(&slugQuery)
|
|
if err == nil {
|
|
url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
|
|
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
|
|
return JSON(200, &dashRedirect)
|
|
}
|
|
log.Warn("Failed to get slug from database, %s", err.Error())
|
|
}
|
|
|
|
filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return Error(500, "Failed to load home dashboard", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
dash := dtos.DashboardFullWithMeta{}
|
|
dash.Meta.IsHome = true
|
|
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
|
|
dash.Meta.FolderTitle = "General"
|
|
|
|
jsonParser := json.NewDecoder(file)
|
|
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
|
return Error(500, "Failed to load home dashboard", err)
|
|
}
|
|
|
|
if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
|
|
addGettingStartedPanelToHomeDashboard(dash.Dashboard)
|
|
}
|
|
|
|
return JSON(200, &dash)
|
|
}
|
|
|
|
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
|
panels := dash.Get("panels").MustArray()
|
|
|
|
newpanel := simplejson.NewFromAny(map[string]interface{}{
|
|
"type": "gettingstarted",
|
|
"id": 123123,
|
|
"gridPos": map[string]interface{}{
|
|
"x": 0,
|
|
"y": 3,
|
|
"w": 24,
|
|
"h": 4,
|
|
},
|
|
})
|
|
|
|
panels = append(panels, newpanel)
|
|
dash.Set("panels", panels)
|
|
}
|
|
|
|
// GetDashboardVersions returns all dashboard versions as JSON
|
|
func GetDashboardVersions(c *m.ReqContext) Response {
|
|
dashID := c.ParamsInt64(":dashboardId")
|
|
|
|
guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
query := m.GetDashboardVersionsQuery{
|
|
OrgId: c.OrgId,
|
|
DashboardId: dashID,
|
|
Limit: c.QueryInt("limit"),
|
|
Start: c.QueryInt("start"),
|
|
}
|
|
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
return Error(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
|
|
}
|
|
|
|
for _, version := range query.Result {
|
|
if version.RestoredFrom == version.Version {
|
|
version.Message = "Initial save (created by migration)"
|
|
continue
|
|
}
|
|
|
|
if version.RestoredFrom > 0 {
|
|
version.Message = fmt.Sprintf("Restored from version %d", version.RestoredFrom)
|
|
continue
|
|
}
|
|
|
|
if version.ParentVersion == 0 {
|
|
version.Message = "Initial save"
|
|
}
|
|
}
|
|
|
|
return JSON(200, query.Result)
|
|
}
|
|
|
|
// GetDashboardVersion returns the dashboard version with the given ID.
|
|
func GetDashboardVersion(c *m.ReqContext) Response {
|
|
dashID := c.ParamsInt64(":dashboardId")
|
|
|
|
guardian := guardian.New(dashID, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
query := m.GetDashboardVersionQuery{
|
|
OrgId: c.OrgId,
|
|
DashboardId: dashID,
|
|
Version: c.ParamsInt(":id"),
|
|
}
|
|
|
|
if err := bus.Dispatch(&query); err != nil {
|
|
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
|
}
|
|
|
|
creator := anonString
|
|
if query.Result.CreatedBy > 0 {
|
|
creator = getUserLogin(query.Result.CreatedBy)
|
|
}
|
|
|
|
dashVersionMeta := &m.DashboardVersionMeta{
|
|
Id: query.Result.Id,
|
|
DashboardId: query.Result.DashboardId,
|
|
Data: query.Result.Data,
|
|
ParentVersion: query.Result.ParentVersion,
|
|
RestoredFrom: query.Result.RestoredFrom,
|
|
Version: query.Result.Version,
|
|
Created: query.Result.Created,
|
|
Message: query.Result.Message,
|
|
CreatedBy: creator,
|
|
}
|
|
|
|
return JSON(200, dashVersionMeta)
|
|
}
|
|
|
|
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
|
|
func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOptions) Response {
|
|
|
|
guardianBase := guardian.New(apiOptions.Base.DashboardId, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardianBase.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
if apiOptions.Base.DashboardId != apiOptions.New.DashboardId {
|
|
guardianNew := guardian.New(apiOptions.New.DashboardId, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardianNew.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
}
|
|
|
|
options := dashdiffs.Options{
|
|
OrgId: c.OrgId,
|
|
DiffType: dashdiffs.ParseDiffType(apiOptions.DiffType),
|
|
Base: dashdiffs.DiffTarget{
|
|
DashboardId: apiOptions.Base.DashboardId,
|
|
Version: apiOptions.Base.Version,
|
|
UnsavedDashboard: apiOptions.Base.UnsavedDashboard,
|
|
},
|
|
New: dashdiffs.DiffTarget{
|
|
DashboardId: apiOptions.New.DashboardId,
|
|
Version: apiOptions.New.Version,
|
|
UnsavedDashboard: apiOptions.New.UnsavedDashboard,
|
|
},
|
|
}
|
|
|
|
result, err := dashdiffs.CalculateDiff(&options)
|
|
if err != nil {
|
|
if err == m.ErrDashboardVersionNotFound {
|
|
return Error(404, "Dashboard version not found", err)
|
|
}
|
|
return Error(500, "Unable to compute diff", err)
|
|
}
|
|
|
|
if options.DiffType == dashdiffs.DiffDelta {
|
|
return Respond(200, result.Delta).Header("Content-Type", "application/json")
|
|
}
|
|
|
|
return Respond(200, result.Delta).Header("Content-Type", "text/html")
|
|
}
|
|
|
|
// RestoreDashboardVersion restores a dashboard to the given version.
|
|
func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
|
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
|
|
if rsp != nil {
|
|
return rsp
|
|
}
|
|
|
|
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
|
|
if err := bus.Dispatch(&versionQuery); err != nil {
|
|
return Error(404, "Dashboard version not found", nil)
|
|
}
|
|
|
|
version := versionQuery.Result
|
|
|
|
saveCmd := m.SaveDashboardCommand{}
|
|
saveCmd.RestoredFrom = version.Version
|
|
saveCmd.OrgId = c.OrgId
|
|
saveCmd.UserId = c.UserId
|
|
saveCmd.Dashboard = version.Data
|
|
saveCmd.Dashboard.Set("version", dash.Version)
|
|
saveCmd.Dashboard.Set("uid", dash.Uid)
|
|
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
|
|
saveCmd.FolderId = dash.FolderId
|
|
|
|
return hs.PostDashboard(c, saveCmd)
|
|
}
|
|
|
|
func GetDashboardTags(c *m.ReqContext) {
|
|
query := m.GetDashboardTagsQuery{OrgId: c.OrgId}
|
|
err := bus.Dispatch(&query)
|
|
if err != nil {
|
|
c.JsonApiErr(500, "Failed to get tags from database", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(200, query.Result)
|
|
}
|