mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Provisioning: Show file path of provisioning file in save/delete dialogs (#16706)
* Add file path to metadata and show it in dialogs * Make path relative to config directory * Fix tests * Add test for the relative path * Refactor to use path relative to provisioner path * Change return types * Rename attribute * Small fixes from review
This commit is contained in:
parent
76ab0aa059
commit
eb82a75668
@ -283,10 +283,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
// Dashboard
|
||||
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
|
||||
dashboardRoute.Get("/uid/:uid", Wrap(GetDashboard))
|
||||
dashboardRoute.Get("/uid/:uid", Wrap(hs.GetDashboard))
|
||||
dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID))
|
||||
|
||||
dashboardRoute.Get("/db/:slug", Wrap(GetDashboard))
|
||||
dashboardRoute.Get("/db/:slug", Wrap(hs.GetDashboard))
|
||||
dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboardBySlug))
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
@ -47,7 +48,7 @@ func dashboardGuardianResponse(err error) Response {
|
||||
return Error(403, "Access denied to this dashboard", nil)
|
||||
}
|
||||
|
||||
func GetDashboard(c *m.ReqContext) Response {
|
||||
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
|
||||
@ -106,14 +107,22 @@ func GetDashboard(c *m.ReqContext) Response {
|
||||
meta.FolderUrl = query.Result.GetUrl()
|
||||
}
|
||||
|
||||
isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
|
||||
err = bus.Dispatch(isDashboardProvisioned)
|
||||
provisioningData, err := dashboards.NewProvisioningService().GetProvisionedDashboardDataByDashboardId(dash.Id)
|
||||
if err != nil {
|
||||
return Error(500, "Error while checking if dashboard is provisioned", err)
|
||||
}
|
||||
|
||||
if isDashboardProvisioned.Result {
|
||||
if provisioningData != nil {
|
||||
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
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@ -43,8 +44,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
|
||||
query.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -198,8 +199,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
fakeDash.HasAcl = true
|
||||
setting.ViewersCanEdit = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
|
||||
query.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -235,6 +236,10 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
// This tests six scenarios:
|
||||
// 1. user is an org viewer AND has no permissions for this dashboard
|
||||
// 2. user is an org editor AND has no permissions for this dashboard
|
||||
@ -247,7 +252,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
role := m.ROLE_VIEWER
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.handlerFunc = hs.GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
@ -260,7 +265,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.handlerFunc = hs.GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
@ -305,7 +310,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
role := m.ROLE_EDITOR
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.handlerFunc = hs.GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by slug", func() {
|
||||
@ -318,7 +323,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
sc.handlerFunc = hs.GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should lookup dashboard by uid", func() {
|
||||
@ -636,8 +641,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
dashTwo.FolderId = 3
|
||||
dashTwo.HasAcl = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
|
||||
query.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -766,8 +771,8 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
|
||||
query.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -905,12 +910,12 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = &m.Dashboard{Id: 1}
|
||||
query.Result = &m.Dashboard{Id: 1, Data: &simplejson.Json{}}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = true
|
||||
bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error {
|
||||
query.Result = &m.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -940,11 +945,32 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
So(result.Get("error").MustString(), ShouldEqual, m.ErrDashboardCannotDeleteProvisionedDashboard.Error())
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
mock := provisioning.NewProvisioningServiceMock()
|
||||
mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string {
|
||||
return "/tmp/grafana/dashboards"
|
||||
}
|
||||
|
||||
dash := GetDashboardShouldReturn200WithConfig(sc, mock)
|
||||
|
||||
Convey("Should return relative path to provisioning file", func() {
|
||||
So(dash.Meta.ProvisionedExternalId, ShouldEqual, "test/dashboard1.json")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||
CallGetDashboard(sc)
|
||||
func GetDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService ProvisioningService) dtos.DashboardFullWithMeta {
|
||||
if provisioningService == nil {
|
||||
provisioningService = provisioning.NewProvisioningServiceMock()
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
ProvisioningService: provisioningService,
|
||||
}
|
||||
CallGetDashboard(sc, hs)
|
||||
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
|
||||
@ -955,8 +981,13 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta
|
||||
return dash
|
||||
}
|
||||
|
||||
func CallGetDashboard(sc *scenarioContext) {
|
||||
sc.handlerFunc = GetDashboard
|
||||
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||
return GetDashboardShouldReturn200WithConfig(sc, nil)
|
||||
}
|
||||
|
||||
func CallGetDashboard(sc *scenarioContext, hs *HTTPServer) {
|
||||
|
||||
sc.handlerFunc = hs.GetDashboard
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
|
@ -7,28 +7,29 @@ import (
|
||||
)
|
||||
|
||||
type DashboardMeta struct {
|
||||
IsStarred bool `json:"isStarred,omitempty"`
|
||||
IsHome bool `json:"isHome,omitempty"`
|
||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanStar bool `json:"canStar"`
|
||||
Slug string `json:"slug"`
|
||||
Url string `json:"url"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version int `json:"version"`
|
||||
HasAcl bool `json:"hasAcl"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
IsStarred bool `json:"isStarred,omitempty"`
|
||||
IsHome bool `json:"isHome,omitempty"`
|
||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CanSave bool `json:"canSave"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanAdmin bool `json:"canAdmin"`
|
||||
CanStar bool `json:"canStar"`
|
||||
Slug string `json:"slug"`
|
||||
Url string `json:"url"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version int `json:"version"`
|
||||
HasAcl bool `json:"hasAcl"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
ProvisionedExternalId string `json:"provisionedExternalId"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
@ -25,13 +25,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/cache"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -42,6 +41,13 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
type ProvisioningService interface {
|
||||
ProvisionDatasources() error
|
||||
ProvisionNotifications() error
|
||||
ProvisionDashboards() error
|
||||
GetDashboardProvisionerResolvedPath(name string) string
|
||||
}
|
||||
|
||||
type HTTPServer struct {
|
||||
log log.Logger
|
||||
macaron *macaron.Macaron
|
||||
@ -49,17 +55,17 @@ type HTTPServer struct {
|
||||
streamManager *live.StreamManager
|
||||
httpSrv *http.Server
|
||||
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService models.UserTokenService `inject:""`
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||
ProvisioningService provisioning.ProvisioningService `inject:""`
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService models.UserTokenService `inject:""`
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
||||
ProvisioningService ProvisioningService `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
|
@ -323,15 +323,13 @@ type GetDashboardSlugByIdQuery struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
type IsDashboardProvisionedQuery struct {
|
||||
type GetProvisionedDashboardDataByIdQuery struct {
|
||||
DashboardId int64
|
||||
|
||||
Result bool
|
||||
Result *DashboardProvisioning
|
||||
}
|
||||
|
||||
type GetProvisionedDashboardDataQuery struct {
|
||||
Name string
|
||||
|
||||
Name string
|
||||
Result []*DashboardProvisioning
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ type DashboardProvisioningService interface {
|
||||
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
||||
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
|
||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||
GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error)
|
||||
UnprovisionDashboard(dashboardId int64) error
|
||||
DeleteProvisionedDashboard(dashboardId int64, orgId int64) error
|
||||
}
|
||||
@ -37,7 +38,9 @@ var NewService = func() DashboardService {
|
||||
|
||||
// NewProvisioningService factory for creating a new dashboard provisioning service
|
||||
var NewProvisioningService = func() DashboardProvisioningService {
|
||||
return &dashboardServiceImpl{}
|
||||
return &dashboardServiceImpl{
|
||||
log: log.New("dashboard-provisioning-service"),
|
||||
}
|
||||
}
|
||||
|
||||
type SaveDashboardDTO struct {
|
||||
@ -65,6 +68,16 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) {
|
||||
cmd := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashboardId}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
|
||||
dash := dto.Dashboard
|
||||
|
||||
@ -123,14 +136,12 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
|
||||
}
|
||||
|
||||
if validateProvisionedDashboard {
|
||||
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
|
||||
err := bus.Dispatch(isDashboardProvisioned)
|
||||
|
||||
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dash.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isDashboardProvisioned.Result {
|
||||
if provisionedData != nil {
|
||||
return nil, models.ErrDashboardCannotSaveProvisionedDashboard
|
||||
}
|
||||
}
|
||||
@ -258,13 +269,12 @@ func (dr *dashboardServiceImpl) DeleteProvisionedDashboard(dashboardId int64, or
|
||||
|
||||
func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64, validateProvisionedDashboard bool) error {
|
||||
if validateProvisionedDashboard {
|
||||
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dashboardId}
|
||||
err := bus.Dispatch(isDashboardProvisioned)
|
||||
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dashboardId)
|
||||
if err != nil {
|
||||
return errutil.Wrap("failed to check if dashboard is provisioned", err)
|
||||
}
|
||||
|
||||
if isDashboardProvisioned.Result {
|
||||
if provisionedData != nil {
|
||||
return models.ErrDashboardCannotDeleteProvisionedDashboard
|
||||
}
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ func TestDashboardService(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = false
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
cmd.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -85,9 +85,9 @@ func TestDashboardService(t *testing.T) {
|
||||
|
||||
Convey("Should return validation error if dashboard is provisioned", func() {
|
||||
provisioningValidated := false
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
provisioningValidated = true
|
||||
cmd.Result = true
|
||||
cmd.Result = &models.DashboardProvisioning{}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -109,8 +109,8 @@ func TestDashboardService(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should return validation error if alert data is invalid", func() {
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = false
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
cmd.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -129,9 +129,9 @@ func TestDashboardService(t *testing.T) {
|
||||
|
||||
Convey("Should not return validation error if dashboard is provisioned", func() {
|
||||
provisioningValidated := false
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
provisioningValidated = true
|
||||
cmd.Result = true
|
||||
cmd.Result = &models.DashboardProvisioning{}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -166,9 +166,9 @@ func TestDashboardService(t *testing.T) {
|
||||
|
||||
Convey("Should return validation error if dashboard is provisioned", func() {
|
||||
provisioningValidated := false
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
provisioningValidated = true
|
||||
cmd.Result = true
|
||||
cmd.Result = &models.DashboardProvisioning{}
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -241,8 +241,12 @@ type Result struct {
|
||||
}
|
||||
|
||||
func setupDeleteHandlers(provisioned bool) *Result {
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = provisioned
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
if provisioned {
|
||||
cmd.Result = &models.DashboardProvisioning{}
|
||||
} else {
|
||||
cmd.Result = nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -112,8 +112,9 @@ func TestFolderService(t *testing.T) {
|
||||
|
||||
provisioningValidated := false
|
||||
|
||||
bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error {
|
||||
bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
provisioningValidated = true
|
||||
query.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -7,18 +7,11 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DashboardProvisioner interface {
|
||||
Provision() error
|
||||
PollChanges(ctx context.Context)
|
||||
}
|
||||
|
||||
type DashboardProvisionerImpl struct {
|
||||
log log.Logger
|
||||
fileReaders []*fileReader
|
||||
}
|
||||
|
||||
type DashboardProvisionerFactory func(string) (DashboardProvisioner, error)
|
||||
|
||||
func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerImpl, error) {
|
||||
logger := log.New("provisioning.dashboard")
|
||||
cfgReader := &configReader{path: configDirectory, log: logger}
|
||||
@ -61,6 +54,17 @@ func (provider *DashboardProvisionerImpl) PollChanges(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetProvisionerResolvedPath returns resolved path for the specified provisioner name. Can be used to generate
|
||||
// relative path to provisioning file from it's external_id.
|
||||
func (provider *DashboardProvisionerImpl) GetProvisionerResolvedPath(name string) string {
|
||||
for _, reader := range provider.fileReaders {
|
||||
if reader.Cfg.Name == name {
|
||||
return reader.resolvedPath()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getFileReaders(configs []*DashboardsAsConfig, logger log.Logger) ([]*fileReader, error) {
|
||||
var readers []*fileReader
|
||||
|
||||
|
@ -3,14 +3,16 @@ package dashboards
|
||||
import "context"
|
||||
|
||||
type Calls struct {
|
||||
Provision []interface{}
|
||||
PollChanges []interface{}
|
||||
Provision []interface{}
|
||||
PollChanges []interface{}
|
||||
GetProvisionerResolvedPath []interface{}
|
||||
}
|
||||
|
||||
type DashboardProvisionerMock struct {
|
||||
Calls *Calls
|
||||
ProvisionFunc func() error
|
||||
PollChangesFunc func(ctx context.Context)
|
||||
Calls *Calls
|
||||
ProvisionFunc func() error
|
||||
PollChangesFunc func(ctx context.Context)
|
||||
GetProvisionerResolvedPathFunc func(name string) string
|
||||
}
|
||||
|
||||
func NewDashboardProvisionerMock() *DashboardProvisionerMock {
|
||||
@ -34,3 +36,12 @@ func (dpm *DashboardProvisionerMock) PollChanges(ctx context.Context) {
|
||||
dpm.PollChangesFunc(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (dpm *DashboardProvisionerMock) GetProvisionerResolvedPath(name string) string {
|
||||
dpm.Calls.PollChanges = append(dpm.Calls.GetProvisionerResolvedPath, name)
|
||||
if dpm.GetProvisionerResolvedPathFunc != nil {
|
||||
return dpm.GetProvisionerResolvedPathFunc(name)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ func (fr *fileReader) pollChanges(ctx context.Context) {
|
||||
// to the database.
|
||||
func (fr *fileReader) startWalkingDisk() error {
|
||||
fr.log.Debug("Start walking disk", "path", fr.Path)
|
||||
resolvedPath := fr.resolvePath(fr.Path)
|
||||
resolvedPath := fr.resolvedPath()
|
||||
if _, err := os.Stat(resolvedPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
@ -329,24 +329,23 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) resolvePath(path string) string {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
func (fr *fileReader) resolvedPath() string {
|
||||
if _, err := os.Stat(fr.Path); os.IsNotExist(err) {
|
||||
fr.log.Error("Cannot read directory", "error", err)
|
||||
}
|
||||
|
||||
copy := path
|
||||
path, err := filepath.Abs(path)
|
||||
path, err := filepath.Abs(fr.Path)
|
||||
if err != nil {
|
||||
fr.log.Error("Could not create absolute path", "path", copy, "error", err)
|
||||
fr.log.Error("Could not create absolute path", "path", fr.Path, "error", err)
|
||||
}
|
||||
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
fr.log.Error("Failed to read content of symlinked path", "path", copy, "error", err)
|
||||
fr.log.Error("Failed to read content of symlinked path", "path", fr.Path, "error", err)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = copy
|
||||
path = fr.Path
|
||||
fr.log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
||||
}
|
||||
return path
|
||||
|
@ -33,7 +33,7 @@ func TestProvsionedSymlinkedFolder(t *testing.T) {
|
||||
t.Errorf("expected err to be nil")
|
||||
}
|
||||
|
||||
resolvedPath := reader.resolvePath(reader.Path)
|
||||
resolvedPath := reader.resolvedPath()
|
||||
if resolvedPath != want {
|
||||
t.Errorf("got %s want %s", resolvedPath, want)
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resolvedPath := reader.resolvePath(reader.Path)
|
||||
resolvedPath := reader.resolvedPath()
|
||||
So(filepath.IsAbs(resolvedPath), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
@ -435,6 +435,10 @@ func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(dashboardI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
|
||||
for _, d := range fakeService.getDashboard {
|
||||
if d.Slug == cmd.Slug {
|
||||
|
@ -15,9 +15,17 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type DashboardProvisioner interface {
|
||||
Provision() error
|
||||
PollChanges(ctx context.Context)
|
||||
GetProvisionerResolvedPath(name string) string
|
||||
}
|
||||
|
||||
type DashboardProvisionerFactory func(string) (DashboardProvisioner, error)
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(NewProvisioningServiceImpl(
|
||||
func(path string) (dashboards.DashboardProvisioner, error) {
|
||||
func(path string) (DashboardProvisioner, error) {
|
||||
return dashboards.NewDashboardProvisionerImpl(path)
|
||||
},
|
||||
notifiers.Provision,
|
||||
@ -25,14 +33,8 @@ func init() {
|
||||
))
|
||||
}
|
||||
|
||||
type ProvisioningService interface {
|
||||
ProvisionDatasources() error
|
||||
ProvisionNotifications() error
|
||||
ProvisionDashboards() error
|
||||
}
|
||||
|
||||
func NewProvisioningServiceImpl(
|
||||
newDashboardProvisioner dashboards.DashboardProvisionerFactory,
|
||||
newDashboardProvisioner DashboardProvisionerFactory,
|
||||
provisionNotifiers func(string) error,
|
||||
provisionDatasources func(string) error,
|
||||
) *provisioningServiceImpl {
|
||||
@ -48,8 +50,8 @@ type provisioningServiceImpl struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
log log.Logger
|
||||
pollingCtxCancel context.CancelFunc
|
||||
newDashboardProvisioner dashboards.DashboardProvisionerFactory
|
||||
dashboardProvisioner dashboards.DashboardProvisioner
|
||||
newDashboardProvisioner DashboardProvisionerFactory
|
||||
dashboardProvisioner DashboardProvisioner
|
||||
provisionNotifiers func(string) error
|
||||
provisionDatasources func(string) error
|
||||
mutex sync.Mutex
|
||||
@ -131,6 +133,10 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *provisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {
|
||||
return ps.dashboardProvisioner.GetProvisionerResolvedPath(name)
|
||||
}
|
||||
|
||||
func (ps *provisioningServiceImpl) cancelPolling() {
|
||||
if ps.pollingCtxCancel != nil {
|
||||
ps.log.Debug("Stop polling for dashboard changes")
|
||||
|
58
pkg/services/provisioning/provisioning_mock.go
Normal file
58
pkg/services/provisioning/provisioning_mock.go
Normal file
@ -0,0 +1,58 @@
|
||||
package provisioning
|
||||
|
||||
type Calls struct {
|
||||
ProvisionDatasources []interface{}
|
||||
ProvisionNotifications []interface{}
|
||||
ProvisionDashboards []interface{}
|
||||
GetDashboardProvisionerResolvedPath []interface{}
|
||||
}
|
||||
|
||||
type ProvisioningServiceMock struct {
|
||||
Calls *Calls
|
||||
ProvisionDatasourcesFunc func() error
|
||||
ProvisionNotificationsFunc func() error
|
||||
ProvisionDashboardsFunc func() error
|
||||
GetDashboardProvisionerResolvedPathFunc func(name string) string
|
||||
}
|
||||
|
||||
func NewProvisioningServiceMock() *ProvisioningServiceMock {
|
||||
return &ProvisioningServiceMock{
|
||||
Calls: &Calls{},
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *ProvisioningServiceMock) ProvisionDatasources() error {
|
||||
mock.Calls.ProvisionDatasources = append(mock.Calls.ProvisionDatasources, nil)
|
||||
if mock.ProvisionDatasourcesFunc != nil {
|
||||
return mock.ProvisionDatasourcesFunc()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *ProvisioningServiceMock) ProvisionNotifications() error {
|
||||
mock.Calls.ProvisionNotifications = append(mock.Calls.ProvisionNotifications, nil)
|
||||
if mock.ProvisionNotificationsFunc != nil {
|
||||
return mock.ProvisionNotificationsFunc()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *ProvisioningServiceMock) ProvisionDashboards() error {
|
||||
mock.Calls.ProvisionDashboards = append(mock.Calls.ProvisionDashboards, nil)
|
||||
if mock.ProvisionDashboardsFunc != nil {
|
||||
return mock.ProvisionDashboardsFunc()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string {
|
||||
mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name)
|
||||
if mock.GetDashboardProvisionerResolvedPathFunc != nil {
|
||||
return mock.GetDashboardProvisionerResolvedPathFunc(name)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
@ -92,7 +92,7 @@ func setup() *serviceTestStruct {
|
||||
}
|
||||
|
||||
serviceTest.service = NewProvisioningServiceImpl(
|
||||
func(path string) (dashboards.DashboardProvisioner, error) {
|
||||
func(path string) (DashboardProvisioner, error) {
|
||||
return serviceTest.mock, nil
|
||||
},
|
||||
nil,
|
||||
|
@ -19,16 +19,16 @@ type DashboardExtras struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
func GetProvisionedDataByDashboardId(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
result := &models.DashboardProvisioning{}
|
||||
|
||||
exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Result = exist
|
||||
|
||||
if exist {
|
||||
cmd.Result = result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -65,20 +65,20 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can query for one provisioned dashboard", func() {
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
|
||||
query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id}
|
||||
|
||||
err := GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(query.Result, ShouldBeTrue)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Can query for none provisioned dashboard", func() {
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: 3000}
|
||||
query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: 3000}
|
||||
|
||||
err := GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
So(query.Result, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Deleting folder should delete provision meta data", func() {
|
||||
@ -89,11 +89,11 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
|
||||
So(DeleteDashboard(deleteCmd), ShouldBeNil)
|
||||
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
|
||||
query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id}
|
||||
|
||||
err = GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
So(query.Result, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("UnprovisionDashboard should delete provisioning metadata", func() {
|
||||
@ -103,11 +103,11 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
|
||||
So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil)
|
||||
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: dashId}
|
||||
query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashId}
|
||||
|
||||
err = GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
So(query.Result, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -27,8 +27,8 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = false
|
||||
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
|
||||
cmd.Result = nil
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -192,6 +192,8 @@ export class SettingsCtrl {
|
||||
text2: `
|
||||
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
|
||||
documentation</a> for more information about provisioning.</i>
|
||||
</br>
|
||||
File path: ${this.dashboard.meta.provisionedExternalId}
|
||||
`,
|
||||
text2htmlBind: true,
|
||||
icon: 'fa-trash',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import angular from 'angular';
|
||||
import { saveAs } from 'file-saver';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from '../../state';
|
||||
|
||||
const template = `
|
||||
<div class="modal-body">
|
||||
@ -21,6 +22,9 @@ const template = `
|
||||
<i>See <a class="external-link" href="http://docs.grafana.org/administration/provisioning/#dashboards" target="_blank">
|
||||
documentation</a> for more information about provisioning.</i>
|
||||
</small>
|
||||
<div class="p-t-1">
|
||||
File path: {{ctrl.dashboardModel.meta.provisionedExternalId}}
|
||||
</div>
|
||||
<div class="p-t-2">
|
||||
<div class="gf-form">
|
||||
<code-editor content="ctrl.dashboardJson" data-mode="json" data-max-lines=15></code-editor>
|
||||
@ -41,12 +45,14 @@ const template = `
|
||||
|
||||
export class SaveProvisionedDashboardModalCtrl {
|
||||
dash: any;
|
||||
dashboardModel: DashboardModel;
|
||||
dashboardJson: string;
|
||||
dismiss: () => void;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(dashboardSrv) {
|
||||
this.dash = dashboardSrv.getCurrent().getSaveModelClone();
|
||||
this.dashboardModel = dashboardSrv.getCurrent();
|
||||
this.dash = this.dashboardModel.getSaveModelClone();
|
||||
delete this.dash.id;
|
||||
this.dashboardJson = angular.toJson(this.dash, true);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ export interface DashboardMeta {
|
||||
canMakeEditable?: boolean;
|
||||
submenuEnabled?: boolean;
|
||||
provisioned?: boolean;
|
||||
provisionedExternalId?: string;
|
||||
focusPanelId?: number;
|
||||
isStarred?: boolean;
|
||||
showSettings?: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user