mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
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>
814 lines
27 KiB
Go
814 lines
27 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/api/apierrors"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/components/dashdiffs"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/coremodel/dashboard"
|
|
"github.com/grafana/grafana/pkg/cuectx"
|
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/alerting"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/guardian"
|
|
pref "github.com/grafana/grafana/pkg/services/preference"
|
|
"github.com/grafana/grafana/pkg/services/star"
|
|
"github.com/grafana/grafana/pkg/services/store"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
const (
|
|
anonString = "Anonymous"
|
|
)
|
|
|
|
func (hs *HTTPServer) isDashboardStarredByUser(c *models.ReqContext, dashID int64) (bool, error) {
|
|
if !c.IsSignedIn {
|
|
return false, nil
|
|
}
|
|
|
|
query := star.IsStarredByUserQuery{UserID: c.UserId, DashboardID: dashID}
|
|
return hs.starService.IsStarredByUser(c.Req.Context(), &query)
|
|
}
|
|
|
|
func dashboardGuardianResponse(err error) response.Response {
|
|
if err != nil {
|
|
return response.Error(500, "Error while checking dashboard permissions", err)
|
|
}
|
|
return response.Error(403, "Access denied to this dashboard", nil)
|
|
}
|
|
|
|
func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response {
|
|
cmd := models.TrimDashboardCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
dash := cmd.Dashboard
|
|
meta := cmd.Meta
|
|
|
|
// TODO temporarily just return the input as a no-op while we convert to thema calls
|
|
dto := dtos.TrimDashboardFullWithMeta{
|
|
Dashboard: dash,
|
|
Meta: meta,
|
|
}
|
|
|
|
c.TimeRequest(metrics.MApiDashboardGet)
|
|
return response.JSON(http.StatusOK, dto)
|
|
}
|
|
|
|
func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
|
|
uid := web.Params(c.Req)[":uid"]
|
|
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgId, 0, uid)
|
|
if rsp != nil {
|
|
return rsp
|
|
}
|
|
// When dash contains only keys id, uid that means dashboard data is not valid and json decode failed.
|
|
if dash.Data != nil {
|
|
isEmptyData := true
|
|
for k := range dash.Data.MustMap() {
|
|
if k != "id" && k != "uid" {
|
|
isEmptyData = false
|
|
break
|
|
}
|
|
}
|
|
if isEmptyData {
|
|
return response.Error(500, "Error while loading dashboard, dashboard data is invalid", nil)
|
|
}
|
|
}
|
|
guardian := guardian.New(c.Req.Context(), 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()
|
|
canDelete, _ := guardian.CanDelete()
|
|
|
|
isStarred, err := hs.isDashboardStarredByUser(c, dash.Id)
|
|
if err != nil {
|
|
return response.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 = hs.getUserLogin(c.Req.Context(), dash.UpdatedBy)
|
|
}
|
|
if dash.CreatedBy > 0 {
|
|
creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy)
|
|
}
|
|
|
|
annotationPermissions := &dtos.AnnotationPermission{}
|
|
|
|
if !hs.AccessControl.IsDisabled() {
|
|
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
|
|
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
|
|
}
|
|
|
|
meta := dtos.DashboardMeta{
|
|
IsStarred: isStarred,
|
|
Slug: dash.Slug,
|
|
Type: models.DashTypeDB,
|
|
CanStar: c.IsSignedIn,
|
|
CanSave: canSave,
|
|
CanEdit: canEdit,
|
|
CanAdmin: canAdmin,
|
|
CanDelete: canDelete,
|
|
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",
|
|
AnnotationsPermissions: annotationPermissions,
|
|
}
|
|
|
|
// lookup folder title
|
|
if dash.FolderId > 0 {
|
|
query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
|
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &query); err != nil {
|
|
if errors.Is(err, models.ErrFolderNotFound) {
|
|
return response.Error(404, "Folder not found", err)
|
|
}
|
|
return response.Error(500, "Dashboard folder could not be read", err)
|
|
}
|
|
meta.FolderUid = query.Result.Uid
|
|
meta.FolderTitle = query.Result.Title
|
|
meta.FolderUrl = query.Result.GetUrl()
|
|
}
|
|
|
|
provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id)
|
|
if err != nil {
|
|
return response.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)
|
|
|
|
// load library panels JSON for this dashboard
|
|
err = hs.LibraryPanelService.LoadLibraryPanelsForDashboard(c.Req.Context(), dash)
|
|
if err != nil {
|
|
return response.Error(500, "Error while loading library panels", err)
|
|
}
|
|
|
|
dto := dtos.DashboardFullWithMeta{
|
|
Dashboard: dash.Data,
|
|
Meta: meta,
|
|
}
|
|
|
|
c.TimeRequest(metrics.MApiDashboardGet)
|
|
return response.JSON(http.StatusOK, dto)
|
|
}
|
|
|
|
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) {
|
|
var err error
|
|
|
|
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope)
|
|
actions.CanAdd, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
|
|
if err != nil {
|
|
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsCreate, "scope", scope)
|
|
}
|
|
|
|
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope)
|
|
actions.CanDelete, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
|
|
if err != nil {
|
|
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope)
|
|
}
|
|
|
|
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, scope)
|
|
actions.CanEdit, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
|
|
if err != nil {
|
|
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsWrite, "scope", scope)
|
|
}
|
|
}
|
|
|
|
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
|
|
query := models.GetUserByIdQuery{Id: userID}
|
|
err := hs.SQLStore.GetUserById(ctx, &query)
|
|
if err != nil {
|
|
return anonString
|
|
}
|
|
return query.Result.Login
|
|
}
|
|
|
|
func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*models.Dashboard, response.Response) {
|
|
var query models.GetDashboardQuery
|
|
|
|
if len(uid) > 0 {
|
|
query = models.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID}
|
|
} else {
|
|
query = models.GetDashboardQuery{Id: id, OrgId: orgID}
|
|
}
|
|
|
|
if err := hs.dashboardService.GetDashboard(ctx, &query); err != nil {
|
|
return nil, response.Error(404, "Dashboard not found", err)
|
|
}
|
|
|
|
return query.Result, nil
|
|
}
|
|
|
|
func (hs *HTTPServer) DeleteDashboardByUID(c *models.ReqContext) response.Response {
|
|
return hs.deleteDashboard(c)
|
|
}
|
|
|
|
func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response {
|
|
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgId, 0, web.Params(c.Req)[":uid"])
|
|
if rsp != nil {
|
|
return rsp
|
|
}
|
|
guardian := guardian.New(c.Req.Context(), dash.Id, c.OrgId, c.SignedInUser)
|
|
if canDelete, err := guardian.CanDelete(); err != nil || !canDelete {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
// disconnect all library elements for this dashboard
|
|
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.Id)
|
|
if err != nil {
|
|
hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err)
|
|
}
|
|
|
|
err = hs.dashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId)
|
|
if err != nil {
|
|
var dashboardErr models.DashboardErr
|
|
if ok := errors.As(err, &dashboardErr); ok {
|
|
if errors.Is(err, models.ErrDashboardCannotDeleteProvisionedDashboard) {
|
|
return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err)
|
|
}
|
|
}
|
|
return response.Error(500, "Failed to delete dashboard", err)
|
|
}
|
|
|
|
if hs.entityEventsService != nil {
|
|
if err := hs.entityEventsService.SaveEvent(c.Req.Context(), store.SaveEventCmd{
|
|
EntityId: store.CreateDatabaseEntityId(dash.Uid, dash.OrgId, store.EntityTypeDashboard),
|
|
EventType: store.EntityEventTypeDelete,
|
|
}); err != nil {
|
|
hs.log.Warn("failed to save dashboard entity event", "uid", dash.Uid, "error", err)
|
|
}
|
|
}
|
|
|
|
if hs.Live != nil {
|
|
err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.OrgId, c.ToUserDisplayDTO(), dash.Uid)
|
|
if err != nil {
|
|
hs.log.Error("Failed to broadcast delete info", "dashboard", dash.Uid, "error", err)
|
|
}
|
|
}
|
|
return response.JSON(http.StatusOK, util.DynMap{
|
|
"title": dash.Title,
|
|
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
|
"id": dash.Id,
|
|
})
|
|
}
|
|
|
|
func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response {
|
|
cmd := models.SaveDashboardCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
|
|
if hs.Features.IsEnabled(featuremgmt.FlagValidateDashboardsOnSave) {
|
|
cm := hs.CoremodelStaticRegistry.Dashboard()
|
|
|
|
// Ideally, coremodel validation calls would be integrated into the web
|
|
// framework. But this does the job for now.
|
|
schv, err := cmd.Dashboard.Get("schemaVersion").Int()
|
|
|
|
// Only try to validate if the schemaVersion is at least the handoff version
|
|
// (the minimum schemaVersion against which the dashboard schema is known to
|
|
// work), or if schemaVersion is absent (which will happen once the Thema
|
|
// schema becomes canonical).
|
|
if err != nil || schv >= dashboard.HandoffSchemaVersion {
|
|
// Can't fail, web.Bind() already ensured it's valid JSON
|
|
b, _ := cmd.Dashboard.Bytes()
|
|
v, _ := cuectx.JSONtoCUE("dashboard.json", b)
|
|
if _, err := cm.CurrentSchema().Validate(v); err != nil {
|
|
return response.Error(http.StatusBadRequest, "invalid dashboard json", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return hs.postDashboard(c, cmd)
|
|
}
|
|
|
|
func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboardCommand) response.Response {
|
|
ctx := c.Req.Context()
|
|
var err error
|
|
cmd.OrgId = c.OrgId
|
|
cmd.UserId = c.UserId
|
|
if cmd.FolderUid != "" {
|
|
folder, err := hs.folderService.GetFolderByUID(ctx, c.SignedInUser, c.OrgId, cmd.FolderUid)
|
|
if err != nil {
|
|
if errors.Is(err, models.ErrFolderNotFound) {
|
|
return response.Error(400, "Folder not found", err)
|
|
}
|
|
return response.Error(500, "Error while checking folder ID", err)
|
|
}
|
|
cmd.FolderId = folder.Id
|
|
}
|
|
|
|
dash := cmd.GetDashboardModel()
|
|
newDashboard := dash.Id == 0
|
|
if newDashboard {
|
|
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
|
if err != nil {
|
|
return response.Error(500, "failed to get quota", err)
|
|
}
|
|
if limitReached {
|
|
return response.Error(403, "Quota reached", nil)
|
|
}
|
|
}
|
|
|
|
var provisioningData *models.DashboardProvisioning
|
|
if dash.Id != 0 {
|
|
data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(dash.Id)
|
|
if err != nil {
|
|
return response.Error(500, "Error while checking if dashboard is provisioned using ID", err)
|
|
}
|
|
provisioningData = data
|
|
} else if dash.Uid != "" {
|
|
data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid)
|
|
if err != nil && !errors.Is(err, models.ErrProvisionedDashboardNotFound) && !errors.Is(err, models.ErrDashboardNotFound) {
|
|
return response.Error(500, "Error while checking if dashboard is provisioned", err)
|
|
}
|
|
provisioningData = data
|
|
}
|
|
|
|
allowUiUpdate := true
|
|
if provisioningData != nil {
|
|
allowUiUpdate = hs.ProvisioningService.GetAllowUIUpdatesFromConfig(provisioningData.Name)
|
|
}
|
|
|
|
// clean up all unnecessary library panels JSON properties so we store a minimum JSON
|
|
err = hs.LibraryPanelService.CleanLibraryPanelsForDashboard(dash)
|
|
if err != nil {
|
|
return response.Error(500, "Error while cleaning library panels", err)
|
|
}
|
|
|
|
dashItem := &dashboards.SaveDashboardDTO{
|
|
Dashboard: dash,
|
|
Message: cmd.Message,
|
|
OrgId: c.OrgId,
|
|
User: c.SignedInUser,
|
|
Overwrite: cmd.Overwrite,
|
|
}
|
|
|
|
dashboard, err := hs.dashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate)
|
|
|
|
if dashboard != nil && hs.entityEventsService != nil {
|
|
if err := hs.entityEventsService.SaveEvent(ctx, store.SaveEventCmd{
|
|
EntityId: store.CreateDatabaseEntityId(dashboard.Uid, dashboard.OrgId, store.EntityTypeDashboard),
|
|
EventType: store.EntityEventTypeUpdate,
|
|
}); err != nil {
|
|
hs.log.Warn("failed to save dashboard entity event", "uid", dashboard.Uid, "error", err)
|
|
}
|
|
}
|
|
|
|
if hs.Live != nil {
|
|
// Tell everyone listening that the dashboard changed
|
|
if dashboard == nil {
|
|
dashboard = dash // the original request
|
|
}
|
|
|
|
// This will broadcast all save requests only if a `gitops` observer exists.
|
|
// gitops is useful when trying to save dashboards in an environment where the user can not save
|
|
channel := hs.Live.GrafanaScope.Dashboards
|
|
liveerr := channel.DashboardSaved(c.SignedInUser.OrgId, c.SignedInUser.ToUserDisplayDTO(), cmd.Message, dashboard, err)
|
|
|
|
// When an error exists, but the value broadcast to a gitops listener return 202
|
|
if liveerr == nil && err != nil && channel.HasGitOpsObserver(c.SignedInUser.OrgId) {
|
|
return response.JSON(202, util.DynMap{
|
|
"status": "pending",
|
|
"message": "changes were broadcast to the gitops listener",
|
|
})
|
|
}
|
|
|
|
if liveerr != nil {
|
|
hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", err)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err)
|
|
}
|
|
|
|
// connect library panels for this dashboard after the dashboard is stored and has an ID
|
|
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(ctx, c.SignedInUser, dashboard)
|
|
if err != nil {
|
|
return response.Error(500, "Error while connecting library panels", err)
|
|
}
|
|
|
|
c.TimeRequest(metrics.MApiDashboardSave)
|
|
return response.JSON(http.StatusOK, util.DynMap{
|
|
"status": "success",
|
|
"slug": dashboard.Slug,
|
|
"version": dashboard.Version,
|
|
"id": dashboard.Id,
|
|
"uid": dashboard.Uid,
|
|
"url": dashboard.GetUrl(),
|
|
})
|
|
}
|
|
|
|
// GetHomeDashboard returns the home dashboard.
|
|
func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
|
|
prefsQuery := pref.GetPreferenceWithDefaultsQuery{OrgID: c.OrgId, UserID: c.SignedInUser.UserId}
|
|
homePage := hs.Cfg.HomePage
|
|
|
|
preference, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
|
|
if err != nil {
|
|
return response.Error(500, "Failed to get preferences", err)
|
|
}
|
|
|
|
if preference.HomeDashboardID == 0 && len(homePage) > 0 {
|
|
homePageRedirect := dtos.DashboardRedirect{RedirectUri: homePage}
|
|
return response.JSON(http.StatusOK, &homePageRedirect)
|
|
}
|
|
|
|
if preference.HomeDashboardID != 0 {
|
|
slugQuery := models.GetDashboardRefByIdQuery{Id: preference.HomeDashboardID}
|
|
err := hs.dashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery)
|
|
if err == nil {
|
|
url := models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
|
|
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
|
|
return response.JSON(http.StatusOK, &dashRedirect)
|
|
}
|
|
hs.log.Warn("Failed to get slug from database", "err", err)
|
|
}
|
|
|
|
filePath := hs.Cfg.DefaultHomeDashboardPath
|
|
if filePath == "" {
|
|
filePath = filepath.Join(hs.Cfg.StaticRootPath, "dashboards/home.json")
|
|
}
|
|
|
|
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
|
|
// variable
|
|
// nolint:gosec
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return response.Error(500, "Failed to load home dashboard", err)
|
|
}
|
|
defer func() {
|
|
if err := file.Close(); err != nil {
|
|
hs.log.Warn("Failed to close dashboard file", "path", filePath, "err", err)
|
|
}
|
|
}()
|
|
|
|
dash := dtos.DashboardFullWithMeta{}
|
|
dash.Meta.IsHome = true
|
|
dash.Meta.CanEdit = c.SignedInUser.HasRole(models.ROLE_EDITOR)
|
|
dash.Meta.FolderTitle = "General"
|
|
dash.Dashboard = simplejson.New()
|
|
|
|
jsonParser := json.NewDecoder(file)
|
|
if err := jsonParser.Decode(dash.Dashboard); err != nil {
|
|
return response.Error(500, "Failed to load home dashboard", err)
|
|
}
|
|
|
|
hs.addGettingStartedPanelToHomeDashboard(c, dash.Dashboard)
|
|
|
|
return response.JSON(http.StatusOK, &dash)
|
|
}
|
|
|
|
func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *models.ReqContext, dash *simplejson.Json) {
|
|
// We only add this getting started panel for Admins who have not dismissed it,
|
|
// and if a custom default home dashboard hasn't been configured
|
|
if !c.HasUserRole(models.ROLE_ADMIN) ||
|
|
c.HasHelpFlag(models.HelpFlagGettingStartedPanelDismissed) ||
|
|
hs.Cfg.DefaultHomeDashboardPath != "" {
|
|
return
|
|
}
|
|
|
|
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": 9,
|
|
},
|
|
})
|
|
|
|
panels = append(panels, newpanel)
|
|
dash.Set("panels", panels)
|
|
}
|
|
|
|
// GetDashboardVersions returns all dashboard versions as JSON
|
|
func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Response {
|
|
var dashID int64
|
|
|
|
var err error
|
|
dashUID := web.Params(c.Req)[":uid"]
|
|
|
|
if dashUID == "" {
|
|
dashID, err = strconv.ParseInt(web.Params(c.Req)[":dashboardId"], 10, 64)
|
|
if err != nil {
|
|
return response.Error(http.StatusBadRequest, "dashboardId is invalid", err)
|
|
}
|
|
} else {
|
|
q := models.GetDashboardQuery{
|
|
OrgId: c.SignedInUser.OrgId,
|
|
Uid: dashUID,
|
|
}
|
|
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &q); err != nil {
|
|
return response.Error(http.StatusBadRequest, "failed to get dashboard by UID", err)
|
|
}
|
|
dashID = q.Result.Id
|
|
}
|
|
guardian := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
query := dashver.ListDashboardVersionsQuery{
|
|
OrgID: c.OrgId,
|
|
DashboardID: dashID,
|
|
DashboardUID: dashUID,
|
|
Limit: c.QueryInt("limit"),
|
|
Start: c.QueryInt("start"),
|
|
}
|
|
|
|
res, err := hs.dashboardVersionService.List(c.Req.Context(), &query)
|
|
if err != nil {
|
|
return response.Error(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
|
|
}
|
|
|
|
for _, version := range res {
|
|
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 response.JSON(http.StatusOK, res)
|
|
}
|
|
|
|
// GetDashboardVersion returns the dashboard version with the given ID.
|
|
func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Response {
|
|
var dashID int64
|
|
|
|
var err error
|
|
dashUID := web.Params(c.Req)[":uid"]
|
|
|
|
if dashUID == "" {
|
|
dashID, err = strconv.ParseInt(web.Params(c.Req)[":dashboardId"], 10, 64)
|
|
if err != nil {
|
|
return response.Error(http.StatusBadRequest, "dashboardId is invalid", err)
|
|
}
|
|
} else {
|
|
q := models.GetDashboardQuery{
|
|
OrgId: c.SignedInUser.OrgId,
|
|
Uid: dashUID,
|
|
}
|
|
if err := hs.dashboardService.GetDashboard(c.Req.Context(), &q); err != nil {
|
|
return response.Error(http.StatusBadRequest, "failed to get dashboard by UID", err)
|
|
}
|
|
dashID = q.Result.Id
|
|
}
|
|
|
|
guardian := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32)
|
|
query := dashver.GetDashboardVersionQuery{
|
|
OrgID: c.OrgId,
|
|
DashboardID: dashID,
|
|
Version: int(version),
|
|
}
|
|
|
|
res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query)
|
|
if err != nil {
|
|
return response.Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
|
}
|
|
|
|
creator := anonString
|
|
if res.CreatedBy > 0 {
|
|
creator = hs.getUserLogin(c.Req.Context(), res.CreatedBy)
|
|
}
|
|
|
|
dashVersionMeta := &dashver.DashboardVersionMeta{
|
|
ID: res.ID,
|
|
DashboardID: res.DashboardID,
|
|
DashboardUID: dashUID,
|
|
Data: res.Data,
|
|
ParentVersion: res.ParentVersion,
|
|
RestoredFrom: res.RestoredFrom,
|
|
Version: res.Version,
|
|
Created: res.Created,
|
|
Message: res.Message,
|
|
CreatedBy: creator,
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, dashVersionMeta)
|
|
}
|
|
|
|
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
|
|
func (hs *HTTPServer) CalculateDashboardDiff(c *models.ReqContext) response.Response {
|
|
apiOptions := dtos.CalculateDiffOptions{}
|
|
if err := web.Bind(c.Req, &apiOptions); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
guardianBase := guardian.New(c.Req.Context(), 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(c.Req.Context(), 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,
|
|
},
|
|
}
|
|
|
|
baseVersionQuery := dashver.GetDashboardVersionQuery{
|
|
DashboardID: options.Base.DashboardId,
|
|
Version: options.Base.Version,
|
|
OrgID: options.OrgId,
|
|
}
|
|
|
|
baseVersionRes, err := hs.dashboardVersionService.Get(c.Req.Context(), &baseVersionQuery)
|
|
if err != nil {
|
|
if errors.Is(err, dashver.ErrDashboardVersionNotFound) {
|
|
return response.Error(404, "Dashboard version not found", err)
|
|
}
|
|
return response.Error(500, "Unable to compute diff", err)
|
|
}
|
|
|
|
newVersionQuery := dashver.GetDashboardVersionQuery{
|
|
DashboardID: options.New.DashboardId,
|
|
Version: options.New.Version,
|
|
OrgID: options.OrgId,
|
|
}
|
|
|
|
newVersionRes, err := hs.dashboardVersionService.Get(c.Req.Context(), &newVersionQuery)
|
|
if err != nil {
|
|
if errors.Is(err, dashver.ErrDashboardVersionNotFound) {
|
|
return response.Error(404, "Dashboard version not found", err)
|
|
}
|
|
return response.Error(500, "Unable to compute diff", err)
|
|
}
|
|
|
|
baseData := baseVersionRes.Data
|
|
newData := newVersionRes.Data
|
|
|
|
result, err := dashdiffs.CalculateDiff(c.Req.Context(), &options, baseData, newData)
|
|
|
|
if err != nil {
|
|
if errors.Is(err, dashver.ErrDashboardVersionNotFound) {
|
|
return response.Error(404, "Dashboard version not found", err)
|
|
}
|
|
return response.Error(500, "Unable to compute diff", err)
|
|
}
|
|
|
|
if options.DiffType == dashdiffs.DiffDelta {
|
|
return response.Respond(http.StatusOK, result.Delta).SetHeader("Content-Type", "application/json")
|
|
}
|
|
|
|
return response.Respond(http.StatusOK, result.Delta).SetHeader("Content-Type", "text/html")
|
|
}
|
|
|
|
// RestoreDashboardVersion restores a dashboard to the given version.
|
|
func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Response {
|
|
var dashID int64
|
|
|
|
var err error
|
|
dashUID := web.Params(c.Req)[":uid"]
|
|
|
|
apiCmd := dtos.RestoreDashboardVersionCommand{}
|
|
if err := web.Bind(c.Req, &apiCmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
if dashUID == "" {
|
|
dashID, err = strconv.ParseInt(web.Params(c.Req)[":dashboardId"], 10, 64)
|
|
if err != nil {
|
|
return response.Error(http.StatusBadRequest, "dashboardId is invalid", err)
|
|
}
|
|
}
|
|
|
|
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgId, dashID, dashUID)
|
|
if rsp != nil {
|
|
return rsp
|
|
}
|
|
|
|
if dash != nil && dash.Id != 0 {
|
|
dashID = dash.Id
|
|
}
|
|
|
|
guardian := guardian.New(c.Req.Context(), dashID, c.OrgId, c.SignedInUser)
|
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
|
return dashboardGuardianResponse(err)
|
|
}
|
|
|
|
versionQuery := dashver.GetDashboardVersionQuery{DashboardID: dashID, Version: apiCmd.Version, OrgID: c.OrgId}
|
|
version, err := hs.dashboardVersionService.Get(c.Req.Context(), &versionQuery)
|
|
if err != nil {
|
|
return response.Error(404, "Dashboard version not found", nil)
|
|
}
|
|
|
|
saveCmd := models.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 (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) {
|
|
query := models.GetDashboardTagsQuery{OrgId: c.OrgId}
|
|
err := hs.dashboardService.GetDashboardTags(c.Req.Context(), &query)
|
|
if err != nil {
|
|
c.JsonApiErr(500, "Failed to get tags from database", err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, query.Result)
|
|
}
|
|
|
|
// GetDashboardUIDs converts internal ids to UIDs
|
|
func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) {
|
|
ids := strings.Split(web.Params(c.Req)[":ids"], ",")
|
|
uids := make([]string, 0, len(ids))
|
|
|
|
q := &models.GetDashboardRefByIdQuery{}
|
|
for _, idstr := range ids {
|
|
id, err := strconv.ParseInt(idstr, 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
q.Id = id
|
|
err = hs.dashboardService.GetDashboardUIDById(c.Req.Context(), q)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
uids = append(uids, q.Result.Uid)
|
|
}
|
|
c.JSON(http.StatusOK, uids)
|
|
}
|