mirror of https://github.com/grafana/grafana.git
				
				
				
			Merge branch 'apps'
This commit is contained in:
		
						commit
						e5b3f27a30
					
				|  | @ -41,8 +41,8 @@ func Register(r *macaron.Macaron) { | ||||||
| 	r.Get("/admin/orgs", reqGrafanaAdmin, Index) | 	r.Get("/admin/orgs", reqGrafanaAdmin, Index) | ||||||
| 	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) | 	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) | ||||||
| 
 | 
 | ||||||
| 	r.Get("/plugins", reqSignedIn, Index) | 	r.Get("/apps", reqSignedIn, Index) | ||||||
| 	r.Get("/plugins/edit/*", reqSignedIn, Index) | 	r.Get("/apps/edit/*", reqSignedIn, Index) | ||||||
| 
 | 
 | ||||||
| 	r.Get("/dashboard/*", reqSignedIn, Index) | 	r.Get("/dashboard/*", reqSignedIn, Index) | ||||||
| 	r.Get("/dashboard-solo/*", reqSignedIn, Index) | 	r.Get("/dashboard-solo/*", reqSignedIn, Index) | ||||||
|  | @ -120,6 +120,11 @@ func Register(r *macaron.Macaron) { | ||||||
| 			r.Get("/invites", wrap(GetPendingOrgInvites)) | 			r.Get("/invites", wrap(GetPendingOrgInvites)) | ||||||
| 			r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) | 			r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) | ||||||
| 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) | 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) | ||||||
|  | 
 | ||||||
|  | 			// apps
 | ||||||
|  | 			r.Get("/apps", wrap(GetOrgAppsList)) | ||||||
|  | 			r.Get("/apps/:appId/settings", wrap(GetAppSettingsById)) | ||||||
|  | 			r.Post("/apps/:appId/settings", bind(m.UpdateAppSettingsCmd{}), wrap(UpdateAppSettings)) | ||||||
| 		}, reqOrgAdmin) | 		}, reqOrgAdmin) | ||||||
| 
 | 
 | ||||||
| 		// create new org
 | 		// create new org
 | ||||||
|  | @ -205,5 +210,7 @@ func Register(r *macaron.Macaron) { | ||||||
| 	// rendering
 | 	// rendering
 | ||||||
| 	r.Get("/render/*", reqSignedIn, RenderToPng) | 	r.Get("/render/*", reqSignedIn, RenderToPng) | ||||||
| 
 | 
 | ||||||
|  | 	InitApiPluginRoutes(r) | ||||||
|  | 
 | ||||||
| 	r.NotFound(NotFoundHandler) | 	r.NotFound(NotFoundHandler) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,75 @@ | ||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httputil" | ||||||
|  | 	"net/url" | ||||||
|  | 
 | ||||||
|  | 	"github.com/Unknwon/macaron" | ||||||
|  | 	"github.com/grafana/grafana/pkg/log" | ||||||
|  | 	"github.com/grafana/grafana/pkg/middleware" | ||||||
|  | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/plugins" | ||||||
|  | 	"github.com/grafana/grafana/pkg/util" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func InitApiPluginRoutes(r *macaron.Macaron) { | ||||||
|  | 	for _, plugin := range plugins.ApiPlugins { | ||||||
|  | 		log.Info("Plugin: Adding proxy routes for api plugin") | ||||||
|  | 		for _, route := range plugin.Routes { | ||||||
|  | 			url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path) | ||||||
|  | 			handlers := make([]macaron.Handler, 0) | ||||||
|  | 			if route.ReqSignedIn { | ||||||
|  | 				handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})) | ||||||
|  | 			} | ||||||
|  | 			if route.ReqGrafanaAdmin { | ||||||
|  | 				handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})) | ||||||
|  | 			} | ||||||
|  | 			if route.ReqSignedIn && route.ReqRole != "" { | ||||||
|  | 				if route.ReqRole == m.ROLE_ADMIN { | ||||||
|  | 					handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN)) | ||||||
|  | 				} else if route.ReqRole == m.ROLE_EDITOR { | ||||||
|  | 					handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			handlers = append(handlers, ApiPlugin(route.Url)) | ||||||
|  | 			r.Route(url, route.Method, handlers...) | ||||||
|  | 			log.Info("Plugin: Adding route %s", url) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ApiPlugin(routeUrl string) macaron.Handler { | ||||||
|  | 	return func(c *middleware.Context) { | ||||||
|  | 		path := c.Params("*") | ||||||
|  | 
 | ||||||
|  | 		//Create a HTTP header with the context in it.
 | ||||||
|  | 		ctx, err := json.Marshal(c.SignedInUser) | ||||||
|  | 		if err != nil { | ||||||
|  | 			c.JsonApiErr(500, "failed to marshal context to json.", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		targetUrl, _ := url.Parse(routeUrl) | ||||||
|  | 		proxy := NewApiPluginProxy(string(ctx), path, targetUrl) | ||||||
|  | 		proxy.Transport = dataProxyTransport | ||||||
|  | 		proxy.ServeHTTP(c.RW(), c.Req.Request) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy { | ||||||
|  | 	director := func(req *http.Request) { | ||||||
|  | 		req.URL.Scheme = targetUrl.Scheme | ||||||
|  | 		req.URL.Host = targetUrl.Host | ||||||
|  | 		req.Host = targetUrl.Host | ||||||
|  | 
 | ||||||
|  | 		req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath) | ||||||
|  | 
 | ||||||
|  | 		// clear cookie headers
 | ||||||
|  | 		req.Header.Del("Cookie") | ||||||
|  | 		req.Header.Del("Set-Cookie") | ||||||
|  | 		req.Header.Add("Grafana-Context", ctx) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &httputil.ReverseProxy{Director: director} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,59 @@ | ||||||
|  | package api | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/grafana/grafana/pkg/api/dtos" | ||||||
|  | 	"github.com/grafana/grafana/pkg/bus" | ||||||
|  | 	"github.com/grafana/grafana/pkg/middleware" | ||||||
|  | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/plugins" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func GetOrgAppsList(c *middleware.Context) Response { | ||||||
|  | 	orgApps, err := plugins.GetOrgAppSettings(c.OrgId) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return ApiError(500, "Failed to list of apps", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	result := make([]*dtos.AppSettings, 0) | ||||||
|  | 	for _, app := range plugins.Apps { | ||||||
|  | 		orgApp := orgApps[app.Id] | ||||||
|  | 		result = append(result, dtos.NewAppSettingsDto(app, orgApp)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return Json(200, result) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetAppSettingsById(c *middleware.Context) Response { | ||||||
|  | 	appId := c.Params(":appId") | ||||||
|  | 
 | ||||||
|  | 	if pluginDef, exists := plugins.Apps[appId]; !exists { | ||||||
|  | 		return ApiError(404, "PluginId not found, no installed plugin with that id", nil) | ||||||
|  | 	} else { | ||||||
|  | 		orgApps, err := plugins.GetOrgAppSettings(c.OrgId) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return ApiError(500, "Failed to get org app settings ", nil) | ||||||
|  | 		} | ||||||
|  | 		orgApp := orgApps[appId] | ||||||
|  | 
 | ||||||
|  | 		return Json(200, dtos.NewAppSettingsDto(pluginDef, orgApp)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func UpdateAppSettings(c *middleware.Context, cmd m.UpdateAppSettingsCmd) Response { | ||||||
|  | 	appId := c.Params(":appId") | ||||||
|  | 
 | ||||||
|  | 	cmd.OrgId = c.OrgId | ||||||
|  | 	cmd.AppId = appId | ||||||
|  | 
 | ||||||
|  | 	if _, ok := plugins.Apps[cmd.AppId]; !ok { | ||||||
|  | 		return ApiError(404, "App type not installed.", nil) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err := bus.Dispatch(&cmd) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return ApiError(500, "Failed to update App Plugin", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ApiSuccess("App updated") | ||||||
|  | } | ||||||
|  | @ -3,6 +3,7 @@ package api | ||||||
| import ( | import ( | ||||||
| 	"github.com/grafana/grafana/pkg/api/dtos" | 	"github.com/grafana/grafana/pkg/api/dtos" | ||||||
| 	"github.com/grafana/grafana/pkg/bus" | 	"github.com/grafana/grafana/pkg/bus" | ||||||
|  | 	//"github.com/grafana/grafana/pkg/log"
 | ||||||
| 	"github.com/grafana/grafana/pkg/middleware" | 	"github.com/grafana/grafana/pkg/middleware" | ||||||
| 	m "github.com/grafana/grafana/pkg/models" | 	m "github.com/grafana/grafana/pkg/models" | ||||||
| 	"github.com/grafana/grafana/pkg/plugins" | 	"github.com/grafana/grafana/pkg/plugins" | ||||||
|  | @ -115,13 +116,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GetDataSourcePlugins(c *middleware.Context) { | func GetDataSourcePlugins(c *middleware.Context) { | ||||||
| 	dsList := make(map[string]interface{}) | 	dsList := make(map[string]*plugins.DataSourcePlugin) | ||||||
| 
 | 
 | ||||||
| 	for key, value := range plugins.DataSources { | 	if enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId); err != nil { | ||||||
| 		if !value.BuiltIn { | 		c.JsonApiErr(500, "Failed to get org apps", err) | ||||||
| 			dsList[key] = value | 		return | ||||||
|  | 	} else { | ||||||
|  | 
 | ||||||
|  | 		for key, value := range enabledPlugins.DataSources { | ||||||
|  | 			if !value.BuiltIn { | ||||||
|  | 				dsList[key] = value | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	c.JSON(200, dsList) | 		c.JSON(200, dsList) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,33 @@ | ||||||
|  | package dtos | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/plugins" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AppSettings struct { | ||||||
|  | 	Name     string                   `json:"name"` | ||||||
|  | 	AppId    string                   `json:"appId"` | ||||||
|  | 	Enabled  bool                     `json:"enabled"` | ||||||
|  | 	Pinned   bool                     `json:"pinned"` | ||||||
|  | 	Info     *plugins.PluginInfo      `json:"info"` | ||||||
|  | 	Pages    []*plugins.AppPluginPage `json:"pages"` | ||||||
|  | 	JsonData map[string]interface{}   `json:"jsonData"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings { | ||||||
|  | 	dto := &AppSettings{ | ||||||
|  | 		AppId: def.Id, | ||||||
|  | 		Name:  def.Name, | ||||||
|  | 		Info:  &def.Info, | ||||||
|  | 		Pages: def.Pages, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if data != nil { | ||||||
|  | 		dto.Enabled = data.Enabled | ||||||
|  | 		dto.Pinned = data.Pinned | ||||||
|  | 		dto.Info = &def.Info | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return dto | ||||||
|  | } | ||||||
|  | @ -8,9 +8,9 @@ type IndexViewData struct { | ||||||
| 	GoogleAnalyticsId  string | 	GoogleAnalyticsId  string | ||||||
| 	GoogleTagManagerId string | 	GoogleTagManagerId string | ||||||
| 
 | 
 | ||||||
| 	PluginCss    []*PluginCss | 	PluginCss     []*PluginCss | ||||||
| 	PluginJs     []string | 	PluginModules []string | ||||||
| 	MainNavLinks []*NavLink | 	MainNavLinks  []*NavLink | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type PluginCss struct { | type PluginCss struct { | ||||||
|  | @ -21,5 +21,6 @@ type PluginCss struct { | ||||||
| type NavLink struct { | type NavLink struct { | ||||||
| 	Text string `json:"text"` | 	Text string `json:"text"` | ||||||
| 	Icon string `json:"icon"` | 	Icon string `json:"icon"` | ||||||
| 	Href string `json:"href"` | 	Img  string `json:"img"` | ||||||
|  | 	Url  string `json:"url"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| package dtos |  | ||||||
| 
 |  | ||||||
| type PluginBundle struct { |  | ||||||
| 	Type     string                 `json:"type"` |  | ||||||
| 	Enabled  bool                   `json:"enabled"` |  | ||||||
| 	Module   string                 `json:"module"` |  | ||||||
| 	JsonData map[string]interface{} `json:"jsonData"` |  | ||||||
| } |  | ||||||
|  | @ -29,6 +29,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro | ||||||
| 	datasources := make(map[string]interface{}) | 	datasources := make(map[string]interface{}) | ||||||
| 	var defaultDatasource string | 	var defaultDatasource string | ||||||
| 
 | 
 | ||||||
|  | 	enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	for _, ds := range orgDataSources { | 	for _, ds := range orgDataSources { | ||||||
| 		url := ds.Url | 		url := ds.Url | ||||||
| 
 | 
 | ||||||
|  | @ -42,7 +47,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro | ||||||
| 			"url":  url, | 			"url":  url, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		meta, exists := plugins.DataSources[ds.Type] | 		meta, exists := enabledPlugins.DataSources[ds.Type] | ||||||
| 		if !exists { | 		if !exists { | ||||||
| 			log.Error(3, "Could not find plugin definition for data source: %v", ds.Type) | 			log.Error(3, "Could not find plugin definition for data source: %v", ds.Type) | ||||||
| 			continue | 			continue | ||||||
|  | @ -110,8 +115,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	panels := map[string]interface{}{} | 	panels := map[string]interface{}{} | ||||||
| 	for _, panel := range plugins.Panels { | 	for _, panel := range enabledPlugins.Panels { | ||||||
| 		panels[panel.Type] = map[string]interface{}{ | 		panels[panel.Id] = map[string]interface{}{ | ||||||
| 			"module": panel.Module, | 			"module": panel.Module, | ||||||
| 			"name":   panel.Name, | 			"name":   panel.Name, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/api/dtos" | 	"github.com/grafana/grafana/pkg/api/dtos" | ||||||
| 	"github.com/grafana/grafana/pkg/middleware" | 	"github.com/grafana/grafana/pkg/middleware" | ||||||
| 	m "github.com/grafana/grafana/pkg/models" | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/plugins" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -50,7 +51,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { | ||||||
| 	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | 	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | ||||||
| 		Text: "Dashboards", | 		Text: "Dashboards", | ||||||
| 		Icon: "fa fa-fw fa-th-large", | 		Icon: "fa fa-fw fa-th-large", | ||||||
| 		Href: "/", | 		Url:  "/", | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | 	data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | ||||||
|  | @ -63,8 +64,37 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { | ||||||
| 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | ||||||
| 			Text: "Data Sources", | 			Text: "Data Sources", | ||||||
| 			Icon: "fa fa-fw fa-database", | 			Icon: "fa fa-fw fa-database", | ||||||
| 			Href: "/datasources", | 			Url:  "/datasources", | ||||||
| 		}) | 		}) | ||||||
|  | 
 | ||||||
|  | 		data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | ||||||
|  | 			Text: "Apps", | ||||||
|  | 			Icon: "fa fa-fw fa-cubes", | ||||||
|  | 			Url:  "/apps", | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, plugin := range enabledPlugins.Apps { | ||||||
|  | 		if plugin.Module != "" { | ||||||
|  | 			data.PluginModules = append(data.PluginModules, plugin.Module) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if plugin.Css != nil { | ||||||
|  | 			data.PluginCss = append(data.PluginCss, &dtos.PluginCss{Light: plugin.Css.Light, Dark: plugin.Css.Dark}) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if plugin.Pinned { | ||||||
|  | 			data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ | ||||||
|  | 				Text: plugin.Name, | ||||||
|  | 				Url:  "/apps/edit/" + plugin.Id, | ||||||
|  | 				Img:  plugin.Info.Logos.Small, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return &data, nil | 	return &data, nil | ||||||
|  |  | ||||||
|  | @ -30,9 +30,9 @@ func newMacaron() *macaron.Macaron { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, route := range plugins.StaticRoutes { | 	for _, route := range plugins.StaticRoutes { | ||||||
| 		pluginRoute := path.Join("/public/plugins/", route.Url) | 		pluginRoute := path.Join("/public/plugins/", route.PluginId) | ||||||
| 		log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path) | 		log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Directory) | ||||||
| 		mapStatic(m, route.Path, "", pluginRoute) | 		mapStatic(m, route.Directory, "", pluginRoute) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mapStatic(m, setting.StaticRootPath, "", "public") | 	mapStatic(m, setting.StaticRootPath, "", "public") | ||||||
|  |  | ||||||
|  | @ -253,3 +253,7 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) { | ||||||
| 
 | 
 | ||||||
| 	ctx.JSON(status, resp) | 	ctx.JSON(status, resp) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (ctx *Context) HasUserRole(role m.RoleType) bool { | ||||||
|  | 	return ctx.OrgRole.Includes(role) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,11 +2,12 @@ package models | ||||||
| 
 | 
 | ||||||
| import "time" | import "time" | ||||||
| 
 | 
 | ||||||
| type PluginBundle struct { | type AppSettings struct { | ||||||
| 	Id       int64 | 	Id       int64 | ||||||
| 	Type     string | 	AppId    string | ||||||
| 	OrgId    int64 | 	OrgId    int64 | ||||||
| 	Enabled  bool | 	Enabled  bool | ||||||
|  | 	Pinned   bool | ||||||
| 	JsonData map[string]interface{} | 	JsonData map[string]interface{} | ||||||
| 
 | 
 | ||||||
| 	Created time.Time | 	Created time.Time | ||||||
|  | @ -17,18 +18,18 @@ type PluginBundle struct { | ||||||
| // COMMANDS
 | // COMMANDS
 | ||||||
| 
 | 
 | ||||||
| // Also acts as api DTO
 | // Also acts as api DTO
 | ||||||
| type UpdatePluginBundleCmd struct { | type UpdateAppSettingsCmd struct { | ||||||
| 	Type     string                 `json:"type" binding:"Required"` |  | ||||||
| 	Enabled  bool                   `json:"enabled"` | 	Enabled  bool                   `json:"enabled"` | ||||||
|  | 	Pinned   bool                   `json:"pinned"` | ||||||
| 	JsonData map[string]interface{} `json:"jsonData"` | 	JsonData map[string]interface{} `json:"jsonData"` | ||||||
| 
 | 
 | ||||||
| 	Id    int64 `json:"-"` | 	AppId string `json:"-"` | ||||||
| 	OrgId int64 `json:"-"` | 	OrgId int64  `json:"-"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ---------------------
 | // ---------------------
 | ||||||
| // QUERIES
 | // QUERIES
 | ||||||
| type GetPluginBundlesQuery struct { | type GetAppSettingsQuery struct { | ||||||
| 	OrgId  int64 | 	OrgId  int64 | ||||||
| 	Result []*PluginBundle | 	Result []*AppSettings | ||||||
| } | } | ||||||
|  | @ -26,6 +26,17 @@ func (r RoleType) IsValid() bool { | ||||||
| 	return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR | 	return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (r RoleType) Includes(other RoleType) bool { | ||||||
|  | 	if r == ROLE_ADMIN { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR { | ||||||
|  | 		return other != ROLE_ADMIN | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return r == other | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type OrgUser struct { | type OrgUser struct { | ||||||
| 	Id      int64 | 	Id      int64 | ||||||
| 	OrgId   int64 | 	OrgId   int64 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,43 @@ | ||||||
|  | package plugins | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 
 | ||||||
|  | 	"github.com/grafana/grafana/pkg/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AppPluginPage struct { | ||||||
|  | 	Name    string          `json:"name"` | ||||||
|  | 	Url     string          `json:"url"` | ||||||
|  | 	ReqRole models.RoleType `json:"reqRole"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AppPluginCss struct { | ||||||
|  | 	Light string `json:"light"` | ||||||
|  | 	Dark  string `json:"dark"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AppPlugin struct { | ||||||
|  | 	FrontendPluginBase | ||||||
|  | 	Css   *AppPluginCss    `json:"css"` | ||||||
|  | 	Pages []*AppPluginPage `json:"pages"` | ||||||
|  | 
 | ||||||
|  | 	Pinned  bool `json:"-"` | ||||||
|  | 	Enabled bool `json:"-"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error { | ||||||
|  | 	if err := decoder.Decode(&app); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if app.Css != nil { | ||||||
|  | 		app.Css.Dark = evalRelativePluginUrlPath(app.Css.Dark, app.Id) | ||||||
|  | 		app.Css.Light = evalRelativePluginUrlPath(app.Css.Light, app.Id) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	app.PluginDir = pluginDir | ||||||
|  | 	app.initFrontendPlugin() | ||||||
|  | 	Apps[app.Id] = app | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | package plugins | ||||||
|  | 
 | ||||||
|  | import "encoding/json" | ||||||
|  | 
 | ||||||
|  | type DataSourcePlugin struct { | ||||||
|  | 	FrontendPluginBase | ||||||
|  | 	DefaultMatchFormat string `json:"defaultMatchFormat"` | ||||||
|  | 	Annotations        bool   `json:"annotations"` | ||||||
|  | 	Metrics            bool   `json:"metrics"` | ||||||
|  | 	BuiltIn            bool   `json:"builtIn"` | ||||||
|  | 	Mixed              bool   `json:"mixed"` | ||||||
|  | 	App                string `json:"app"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error { | ||||||
|  | 	if err := decoder.Decode(&p); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	p.PluginDir = pluginDir | ||||||
|  | 	p.initFrontendPlugin() | ||||||
|  | 	DataSources[p.Id] = p | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | package plugins | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/url" | ||||||
|  | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type FrontendPluginBase struct { | ||||||
|  | 	PluginBase | ||||||
|  | 	Module     string `json:"module"` | ||||||
|  | 	StaticRoot string `json:"staticRoot"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (fp *FrontendPluginBase) initFrontendPlugin() { | ||||||
|  | 	if fp.StaticRoot != "" { | ||||||
|  | 		StaticRoutes = append(StaticRoutes, &PluginStaticRoute{ | ||||||
|  | 			Directory: filepath.Join(fp.PluginDir, fp.StaticRoot), | ||||||
|  | 			PluginId:  fp.Id, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id) | ||||||
|  | 	fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id) | ||||||
|  | 
 | ||||||
|  | 	fp.handleModuleDefaults() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (fp *FrontendPluginBase) handleModuleDefaults() { | ||||||
|  | 	if fp.Module != "" { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if fp.StaticRoot != "" { | ||||||
|  | 		fp.Module = path.Join("plugins", fp.Id, "module") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func evalRelativePluginUrlPath(pathStr string, pluginId string) string { | ||||||
|  | 	u, _ := url.Parse(pathStr) | ||||||
|  | 	if u.IsAbs() { | ||||||
|  | 		return pathStr | ||||||
|  | 	} | ||||||
|  | 	return path.Join("public/plugins", pluginId, pathStr) | ||||||
|  | } | ||||||
|  | @ -1,26 +1,74 @@ | ||||||
| package plugins | package plugins | ||||||
| 
 | 
 | ||||||
| type DataSourcePlugin struct { | import ( | ||||||
| 	Type               string                 `json:"type"` | 	"encoding/json" | ||||||
| 	Name               string                 `json:"name"` | 
 | ||||||
| 	ServiceName        string                 `json:"serviceName"` | 	"github.com/grafana/grafana/pkg/models" | ||||||
| 	Module             string                 `json:"module"` | ) | ||||||
| 	Partials           map[string]interface{} `json:"partials"` | 
 | ||||||
| 	DefaultMatchFormat string                 `json:"defaultMatchFormat"` | type PluginLoader interface { | ||||||
| 	Annotations        bool                   `json:"annotations"` | 	Load(decoder *json.Decoder, pluginDir string) error | ||||||
| 	Metrics            bool                   `json:"metrics"` |  | ||||||
| 	BuiltIn            bool                   `json:"builtIn"` |  | ||||||
| 	StaticRootConfig   *StaticRootConfig      `json:"staticRoot"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type PanelPlugin struct { | type PluginBase struct { | ||||||
| 	Type             string            `json:"type"` | 	Type      string     `json:"type"` | ||||||
| 	Name             string            `json:"name"` | 	Name      string     `json:"name"` | ||||||
| 	Module           string            `json:"module"` | 	Id        string     `json:"id"` | ||||||
| 	StaticRootConfig *StaticRootConfig `json:"staticRoot"` | 	App       string     `json:"app"` | ||||||
|  | 	Info      PluginInfo `json:"info"` | ||||||
|  | 	PluginDir string     `json:"-"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type StaticRootConfig struct { | type PluginInfo struct { | ||||||
|  | 	Author      PluginInfoLink   `json:"author"` | ||||||
|  | 	Description string           `json:"description"` | ||||||
|  | 	Links       []PluginInfoLink `json:"links"` | ||||||
|  | 	Logos       PluginLogos      `json:"logos"` | ||||||
|  | 	Version     string           `json:"version"` | ||||||
|  | 	Updated     string           `json:"updated"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type PluginInfoLink struct { | ||||||
|  | 	Name string `json:"name"` | ||||||
| 	Url  string `json:"url"` | 	Url  string `json:"url"` | ||||||
| 	Path string `json:"path"` | } | ||||||
|  | 
 | ||||||
|  | type PluginLogos struct { | ||||||
|  | 	Small string `json:"small"` | ||||||
|  | 	Large string `json:"large"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type PluginStaticRoute struct { | ||||||
|  | 	Directory string | ||||||
|  | 	PluginId  string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ApiPluginRoute struct { | ||||||
|  | 	Path            string          `json:"path"` | ||||||
|  | 	Method          string          `json:"method"` | ||||||
|  | 	ReqSignedIn     bool            `json:"reqSignedIn"` | ||||||
|  | 	ReqGrafanaAdmin bool            `json:"reqGrafanaAdmin"` | ||||||
|  | 	ReqRole         models.RoleType `json:"reqRole"` | ||||||
|  | 	Url             string          `json:"url"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ApiPlugin struct { | ||||||
|  | 	PluginBase | ||||||
|  | 	Routes []*ApiPluginRoute `json:"routes"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type EnabledPlugins struct { | ||||||
|  | 	Panels      []*PanelPlugin | ||||||
|  | 	DataSources map[string]*DataSourcePlugin | ||||||
|  | 	ApiList     []*ApiPlugin | ||||||
|  | 	Apps        []*AppPlugin | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewEnabledPlugins() EnabledPlugins { | ||||||
|  | 	return EnabledPlugins{ | ||||||
|  | 		Panels:      make([]*PanelPlugin, 0), | ||||||
|  | 		DataSources: make(map[string]*DataSourcePlugin), | ||||||
|  | 		ApiList:     make([]*ApiPlugin, 0), | ||||||
|  | 		Apps:        make([]*AppPlugin, 0), | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | package plugins | ||||||
|  | 
 | ||||||
|  | import "encoding/json" | ||||||
|  | 
 | ||||||
|  | type PanelPlugin struct { | ||||||
|  | 	FrontendPluginBase | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error { | ||||||
|  | 	if err := decoder.Decode(&p); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	p.PluginDir = pluginDir | ||||||
|  | 	p.initFrontendPlugin() | ||||||
|  | 	Panels[p.Id] = p | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -1,12 +1,16 @@ | ||||||
| package plugins | package plugins | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"text/template" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/log" | 	"github.com/grafana/grafana/pkg/log" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
|  | @ -14,9 +18,12 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	DataSources  map[string]DataSourcePlugin | 	DataSources  map[string]*DataSourcePlugin | ||||||
| 	Panels       map[string]PanelPlugin | 	Panels       map[string]*PanelPlugin | ||||||
| 	StaticRoutes []*StaticRootConfig | 	ApiPlugins   map[string]*ApiPlugin | ||||||
|  | 	StaticRoutes []*PluginStaticRoute | ||||||
|  | 	Apps         map[string]*AppPlugin | ||||||
|  | 	PluginTypes  map[string]interface{} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type PluginScanner struct { | type PluginScanner struct { | ||||||
|  | @ -25,18 +32,45 @@ type PluginScanner struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Init() error { | func Init() error { | ||||||
| 	DataSources = make(map[string]DataSourcePlugin) | 	DataSources = make(map[string]*DataSourcePlugin) | ||||||
| 	StaticRoutes = make([]*StaticRootConfig, 0) | 	ApiPlugins = make(map[string]*ApiPlugin) | ||||||
| 	Panels = make(map[string]PanelPlugin) | 	StaticRoutes = make([]*PluginStaticRoute, 0) | ||||||
|  | 	Panels = make(map[string]*PanelPlugin) | ||||||
|  | 	Apps = make(map[string]*AppPlugin) | ||||||
|  | 	PluginTypes = map[string]interface{}{ | ||||||
|  | 		"panel":      PanelPlugin{}, | ||||||
|  | 		"datasource": DataSourcePlugin{}, | ||||||
|  | 		"api":        ApiPlugin{}, | ||||||
|  | 		"app":        AppPlugin{}, | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	scan(path.Join(setting.StaticRootPath, "app/plugins")) | 	scan(path.Join(setting.StaticRootPath, "app/plugins")) | ||||||
| 	scan(path.Join(setting.PluginsPath)) | 	checkPluginPaths() | ||||||
| 	checkExternalPluginPaths() | 	// checkDependencies()
 | ||||||
| 
 |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func checkExternalPluginPaths() error { | // func checkDependencies() {
 | ||||||
|  | // 	for appType, app := range Apps {
 | ||||||
|  | // 		for _, reqPanel := range app.PanelPlugins {
 | ||||||
|  | // 			if _, ok := Panels[reqPanel]; !ok {
 | ||||||
|  | // 				log.Fatal(4, "App %s requires Panel type %s, but it is not present.", appType, reqPanel)
 | ||||||
|  | // 			}
 | ||||||
|  | // 		}
 | ||||||
|  | // 		for _, reqDataSource := range app.DatasourcePlugins {
 | ||||||
|  | // 			if _, ok := DataSources[reqDataSource]; !ok {
 | ||||||
|  | // 				log.Fatal(4, "App %s requires DataSource type %s, but it is not present.", appType, reqDataSource)
 | ||||||
|  | // 			}
 | ||||||
|  | // 		}
 | ||||||
|  | // 		for _, reqApiPlugin := range app.ApiPlugins {
 | ||||||
|  | // 			if _, ok := ApiPlugins[reqApiPlugin]; !ok {
 | ||||||
|  | // 				log.Fatal(4, "App %s requires ApiPlugin type %s, but it is not present.", appType, reqApiPlugin)
 | ||||||
|  | // 			}
 | ||||||
|  | // 		}
 | ||||||
|  | // 	}
 | ||||||
|  | // }
 | ||||||
|  | 
 | ||||||
|  | func checkPluginPaths() error { | ||||||
| 	for _, section := range setting.Cfg.Sections() { | 	for _, section := range setting.Cfg.Sections() { | ||||||
| 		if strings.HasPrefix(section.Name(), "plugin.") { | 		if strings.HasPrefix(section.Name(), "plugin.") { | ||||||
| 			path := section.Key("path").String() | 			path := section.Key("path").String() | ||||||
|  | @ -87,11 +121,26 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) { | func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) { | ||||||
| 	if staticRootConfig != nil { | 	buf := new(bytes.Buffer) | ||||||
| 		staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path) | 	buf.ReadFrom(reader) | ||||||
| 		StaticRoutes = append(StaticRoutes, staticRootConfig) | 	jsonStr := buf.String() //
 | ||||||
|  | 
 | ||||||
|  | 	tmpl, err := template.New("json").Parse(jsonStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	data := map[string]interface{}{ | ||||||
|  | 		"PluginPublicRoot": "public/plugins/" + pluginCommon.Id, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var resultBuffer bytes.Buffer | ||||||
|  | 	if err := tmpl.ExecuteTemplate(&resultBuffer, "json", data); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return bytes.NewReader(resultBuffer.Bytes()), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { | func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { | ||||||
|  | @ -104,46 +153,29 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { | ||||||
| 	defer reader.Close() | 	defer reader.Close() | ||||||
| 
 | 
 | ||||||
| 	jsonParser := json.NewDecoder(reader) | 	jsonParser := json.NewDecoder(reader) | ||||||
| 
 | 	pluginCommon := PluginBase{} | ||||||
| 	pluginJson := make(map[string]interface{}) | 	if err := jsonParser.Decode(&pluginCommon); err != nil { | ||||||
| 	if err := jsonParser.Decode(&pluginJson); err != nil { |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	pluginType, exists := pluginJson["pluginType"] | 	if pluginCommon.Id == "" || pluginCommon.Type == "" { | ||||||
| 	if !exists { | 		return errors.New("Did not find type and id property in plugin.json") | ||||||
| 		return errors.New("Did not find pluginType property in plugin.json") |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if pluginType == "datasource" { | 	reader.Seek(0, 0) | ||||||
| 		p := DataSourcePlugin{} | 	if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil { | ||||||
| 		reader.Seek(0, 0) | 		return err | ||||||
| 		if err := jsonParser.Decode(&p); err != nil { | 	} else { | ||||||
| 			return err | 		jsonParser = json.NewDecoder(newReader) | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if p.Type == "" { |  | ||||||
| 			return errors.New("Did not find type property in plugin.json") |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		DataSources[p.Type] = p |  | ||||||
| 		addStaticRoot(p.StaticRootConfig, currentDir) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if pluginType == "panel" { | 	var loader PluginLoader | ||||||
| 		p := PanelPlugin{} |  | ||||||
| 		reader.Seek(0, 0) |  | ||||||
| 		if err := jsonParser.Decode(&p); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		if p.Type == "" { | 	if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists { | ||||||
| 			return errors.New("Did not find type property in plugin.json") | 		return errors.New("Unkown plugin type " + pluginCommon.Type) | ||||||
| 		} | 	} else { | ||||||
| 
 | 		loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) | ||||||
| 		Panels[p.Type] = p |  | ||||||
| 		addStaticRoot(p.StaticRootConfig, currentDir) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return loader.Load(jsonParser, currentDir) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -18,5 +18,22 @@ func TestPluginScans(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		So(err, ShouldBeNil) | 		So(err, ShouldBeNil) | ||||||
| 		So(len(DataSources), ShouldBeGreaterThan, 1) | 		So(len(DataSources), ShouldBeGreaterThan, 1) | ||||||
|  | 		So(len(Panels), ShouldBeGreaterThan, 1) | ||||||
|  | 
 | ||||||
|  | 		Convey("Should set module automatically", func() { | ||||||
|  | 			So(DataSources["graphite"].Module, ShouldEqual, "app/plugins/datasource/graphite/module") | ||||||
|  | 		}) | ||||||
| 	}) | 	}) | ||||||
|  | 
 | ||||||
|  | 	Convey("When reading app plugin definition", t, func() { | ||||||
|  | 		setting.Cfg = ini.Empty() | ||||||
|  | 		sec, _ := setting.Cfg.NewSection("plugin.app-test") | ||||||
|  | 		sec.NewKey("path", "../../tests/app-plugin-json") | ||||||
|  | 		err := Init() | ||||||
|  | 
 | ||||||
|  | 		So(err, ShouldBeNil) | ||||||
|  | 		So(len(Apps), ShouldBeGreaterThan, 0) | ||||||
|  | 		So(Apps["app-example"].Info.Logos.Large, ShouldEqual, "public/plugins/app-example/img/logo_large.png") | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,75 @@ | ||||||
|  | package plugins | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/grafana/grafana/pkg/bus" | ||||||
|  | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func GetOrgAppSettings(orgId int64) (map[string]*m.AppSettings, error) { | ||||||
|  | 	query := m.GetAppSettingsQuery{OrgId: orgId} | ||||||
|  | 
 | ||||||
|  | 	if err := bus.Dispatch(&query); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	orgAppsMap := make(map[string]*m.AppSettings) | ||||||
|  | 	for _, orgApp := range query.Result { | ||||||
|  | 		orgAppsMap[orgApp.AppId] = orgApp | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return orgAppsMap, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) { | ||||||
|  | 	enabledPlugins := NewEnabledPlugins() | ||||||
|  | 	orgApps, err := GetOrgAppSettings(orgId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	seenPanels := make(map[string]bool) | ||||||
|  | 	seenApi := make(map[string]bool) | ||||||
|  | 
 | ||||||
|  | 	for appId, installedApp := range Apps { | ||||||
|  | 		var app AppPlugin | ||||||
|  | 		app = *installedApp | ||||||
|  | 
 | ||||||
|  | 		// check if the app is stored in the DB for this org and if so, use the
 | ||||||
|  | 		// state stored there.
 | ||||||
|  | 		if b, ok := orgApps[appId]; ok { | ||||||
|  | 			app.Enabled = b.Enabled | ||||||
|  | 			app.Pinned = b.Pinned | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if app.Enabled { | ||||||
|  | 			enabledPlugins.Apps = append(enabledPlugins.Apps, &app) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// add all plugins that are not part of an App.
 | ||||||
|  | 	for d, installedDs := range DataSources { | ||||||
|  | 		if installedDs.App == "" { | ||||||
|  | 			enabledPlugins.DataSources[d] = installedDs | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for p, panel := range Panels { | ||||||
|  | 		if panel.App == "" { | ||||||
|  | 			if _, ok := seenPanels[p]; !ok { | ||||||
|  | 				seenPanels[p] = true | ||||||
|  | 				enabledPlugins.Panels = append(enabledPlugins.Panels, panel) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for a, api := range ApiPlugins { | ||||||
|  | 		if api.App == "" { | ||||||
|  | 			if _, ok := seenApi[a]; !ok { | ||||||
|  | 				seenApi[a] = true | ||||||
|  | 				enabledPlugins.ApiList = append(enabledPlugins.ApiList, api) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &enabledPlugins, nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,50 @@ | ||||||
|  | package sqlstore | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/grafana/grafana/pkg/bus" | ||||||
|  | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	bus.AddHandler("sql", GetAppSettings) | ||||||
|  | 	bus.AddHandler("sql", UpdateAppSettings) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetAppSettings(query *m.GetAppSettingsQuery) error { | ||||||
|  | 	sess := x.Where("org_id=?", query.OrgId) | ||||||
|  | 
 | ||||||
|  | 	query.Result = make([]*m.AppSettings, 0) | ||||||
|  | 	return sess.Find(&query.Result) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error { | ||||||
|  | 	return inTransaction2(func(sess *session) error { | ||||||
|  | 		var app m.AppSettings | ||||||
|  | 
 | ||||||
|  | 		exists, err := sess.Where("org_id=? and app_id=?", cmd.OrgId, cmd.AppId).Get(&app) | ||||||
|  | 		sess.UseBool("enabled") | ||||||
|  | 		sess.UseBool("pinned") | ||||||
|  | 		if !exists { | ||||||
|  | 			app = m.AppSettings{ | ||||||
|  | 				AppId:    cmd.AppId, | ||||||
|  | 				OrgId:    cmd.OrgId, | ||||||
|  | 				Enabled:  cmd.Enabled, | ||||||
|  | 				Pinned:   cmd.Pinned, | ||||||
|  | 				JsonData: cmd.JsonData, | ||||||
|  | 				Created:  time.Now(), | ||||||
|  | 				Updated:  time.Now(), | ||||||
|  | 			} | ||||||
|  | 			_, err = sess.Insert(&app) | ||||||
|  | 			return err | ||||||
|  | 		} else { | ||||||
|  | 			app.Updated = time.Now() | ||||||
|  | 			app.Enabled = cmd.Enabled | ||||||
|  | 			app.JsonData = cmd.JsonData | ||||||
|  | 			app.Pinned = cmd.Pinned | ||||||
|  | 			_, err = sess.Id(app.Id).Update(&app) | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -2,25 +2,27 @@ package migrations | ||||||
| 
 | 
 | ||||||
| import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" | import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" | ||||||
| 
 | 
 | ||||||
| func addPluginBundleMigration(mg *Migrator) { | func addAppSettingsMigration(mg *Migrator) { | ||||||
| 
 | 
 | ||||||
| 	var pluginBundleV1 = Table{ | 	appSettingsV1 := Table{ | ||||||
| 		Name: "plugin_bundle", | 		Name: "app_settings", | ||||||
| 		Columns: []*Column{ | 		Columns: []*Column{ | ||||||
| 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, | 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, | ||||||
| 			{Name: "org_id", Type: DB_BigInt, Nullable: true}, | 			{Name: "org_id", Type: DB_BigInt, Nullable: true}, | ||||||
| 			{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, | 			{Name: "app_id", Type: DB_NVarchar, Length: 255, Nullable: false}, | ||||||
| 			{Name: "enabled", Type: DB_Bool, Nullable: false}, | 			{Name: "enabled", Type: DB_Bool, Nullable: false}, | ||||||
|  | 			{Name: "pinned", Type: DB_Bool, Nullable: false}, | ||||||
| 			{Name: "json_data", Type: DB_Text, Nullable: true}, | 			{Name: "json_data", Type: DB_Text, Nullable: true}, | ||||||
| 			{Name: "created", Type: DB_DateTime, Nullable: false}, | 			{Name: "created", Type: DB_DateTime, Nullable: false}, | ||||||
| 			{Name: "updated", Type: DB_DateTime, Nullable: false}, | 			{Name: "updated", Type: DB_DateTime, Nullable: false}, | ||||||
| 		}, | 		}, | ||||||
| 		Indices: []*Index{ | 		Indices: []*Index{ | ||||||
| 			{Cols: []string{"org_id", "type"}, Type: UniqueIndex}, | 			{Cols: []string{"org_id", "app_id"}, Type: UniqueIndex}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	mg.AddMigration("create plugin_bundle table v1", NewAddTableMigration(pluginBundleV1)) | 
 | ||||||
|  | 	mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1)) | ||||||
| 
 | 
 | ||||||
| 	//-------  indexes ------------------
 | 	//-------  indexes ------------------
 | ||||||
| 	addTableIndicesMigrations(mg, "v1", pluginBundleV1) | 	addTableIndicesMigrations(mg, "v3", appSettingsV1) | ||||||
| } | } | ||||||
|  | @ -18,7 +18,7 @@ func AddMigrations(mg *Migrator) { | ||||||
| 	addApiKeyMigrations(mg) | 	addApiKeyMigrations(mg) | ||||||
| 	addDashboardSnapshotMigrations(mg) | 	addDashboardSnapshotMigrations(mg) | ||||||
| 	addQuotaMigration(mg) | 	addQuotaMigration(mg) | ||||||
| 	addPluginBundleMigration(mg) | 	addAppSettingsMigration(mg) | ||||||
| 	addSessionMigration(mg) | 	addSessionMigration(mg) | ||||||
| 	addPlaylistMigrations(mg) | 	addPlaylistMigrations(mg) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,46 +0,0 @@ | ||||||
| package sqlstore |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/grafana/grafana/pkg/bus" |  | ||||||
| 	m "github.com/grafana/grafana/pkg/models" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	bus.AddHandler("sql", GetPluginBundles) |  | ||||||
| 	bus.AddHandler("sql", UpdatePluginBundle) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func GetPluginBundles(query *m.GetPluginBundlesQuery) error { |  | ||||||
| 	sess := x.Where("org_id=?", query.OrgId) |  | ||||||
| 
 |  | ||||||
| 	query.Result = make([]*m.PluginBundle, 0) |  | ||||||
| 	return sess.Find(&query.Result) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func UpdatePluginBundle(cmd *m.UpdatePluginBundleCmd) error { |  | ||||||
| 	return inTransaction2(func(sess *session) error { |  | ||||||
| 		var bundle m.PluginBundle |  | ||||||
| 
 |  | ||||||
| 		exists, err := sess.Where("org_id=? and type=?", cmd.OrgId, cmd.Type).Get(&bundle) |  | ||||||
| 		sess.UseBool("enabled") |  | ||||||
| 		if !exists { |  | ||||||
| 			bundle = m.PluginBundle{ |  | ||||||
| 				Type:     cmd.Type, |  | ||||||
| 				OrgId:    cmd.OrgId, |  | ||||||
| 				Enabled:  cmd.Enabled, |  | ||||||
| 				JsonData: cmd.JsonData, |  | ||||||
| 				Created:  time.Now(), |  | ||||||
| 				Updated:  time.Now(), |  | ||||||
| 			} |  | ||||||
| 			_, err = sess.Insert(&bundle) |  | ||||||
| 			return err |  | ||||||
| 		} else { |  | ||||||
| 			bundle.Enabled = cmd.Enabled |  | ||||||
| 			bundle.JsonData = cmd.JsonData |  | ||||||
| 			_, err = sess.Id(bundle.Id).Update(&bundle) |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  | @ -1,9 +1,3 @@ | ||||||
| // import grafanaCtrl from './grafana_ctrl';
 |  | ||||||
| //
 |  | ||||||
| // import * as asd from './sidemenu_ctrl';
 |  | ||||||
| //
 |  | ||||||
| // export {grafanaCtrl};
 |  | ||||||
| 
 |  | ||||||
| define([ | define([ | ||||||
|   './grafana_ctrl', |   './grafana_ctrl', | ||||||
|   './search_ctrl', |   './search_ctrl', | ||||||
|  |  | ||||||
|  | @ -19,7 +19,8 @@ function (angular, _, $, coreModule, config) { | ||||||
|         $scope.mainLinks.push({ |         $scope.mainLinks.push({ | ||||||
|           text: item.text, |           text: item.text, | ||||||
|           icon: item.icon, |           icon: item.icon, | ||||||
|           href: $scope.getUrl(item.href) |           img: item.img, | ||||||
|  |           url: $scope.getUrl(item.url) | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | @ -62,12 +62,13 @@ function (angular, coreModule, kbn) { | ||||||
|         var label = '<label for="' + scope.$id + model + '" class="checkbox-label">' + |         var label = '<label for="' + scope.$id + model + '" class="checkbox-label">' + | ||||||
|           text + tip + '</label>'; |           text + tip + '</label>'; | ||||||
| 
 | 
 | ||||||
|         var template = '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' + |         var template = | ||||||
|  |           '<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' + | ||||||
|           '       ng-model="' + model + '"' + ngchange + |           '       ng-model="' + model + '"' + ngchange + | ||||||
|           '       ng-checked="' + model + '"></input>' + |           '       ng-checked="' + model + '"></input>' + | ||||||
|           ' <label for="' + scope.$id + model + '" class="cr1"></label>'; |           ' <label for="' + scope.$id + model + '" class="cr1"></label>'; | ||||||
| 
 | 
 | ||||||
|         template = label + template; |         template = template + label; | ||||||
|         elem.replaceWith($compile(angular.element(template))(scope)); |         elem.replaceWith($compile(angular.element(template))(scope)); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ define([ | ||||||
|     $locationProvider.html5Mode(true); |     $locationProvider.html5Mode(true); | ||||||
| 
 | 
 | ||||||
|     var loadOrgBundle = new BundleLoader.BundleLoader('app/features/org/all'); |     var loadOrgBundle = new BundleLoader.BundleLoader('app/features/org/all'); | ||||||
|  |     var loadAppsBundle = new BundleLoader.BundleLoader('app/features/apps/all'); | ||||||
| 
 | 
 | ||||||
|     $routeProvider |     $routeProvider | ||||||
|       .when('/', { |       .when('/', { | ||||||
|  | @ -41,17 +42,17 @@ define([ | ||||||
|         controller : 'DashboardImportCtrl', |         controller : 'DashboardImportCtrl', | ||||||
|       }) |       }) | ||||||
|       .when('/datasources', { |       .when('/datasources', { | ||||||
|         templateUrl: 'app/features/org/partials/datasources.html', |         templateUrl: 'app/features/datasources/partials/list.html', | ||||||
|         controller : 'DataSourcesCtrl', |         controller : 'DataSourcesCtrl', | ||||||
|         resolve: loadOrgBundle, |         resolve: loadOrgBundle, | ||||||
|       }) |       }) | ||||||
|       .when('/datasources/edit/:id', { |       .when('/datasources/edit/:id', { | ||||||
|         templateUrl: 'app/features/org/partials/datasourceEdit.html', |         templateUrl: 'app/features/datasources/partials/edit.html', | ||||||
|         controller : 'DataSourceEditCtrl', |         controller : 'DataSourceEditCtrl', | ||||||
|         resolve: loadOrgBundle, |         resolve: loadOrgBundle, | ||||||
|       }) |       }) | ||||||
|       .when('/datasources/new', { |       .when('/datasources/new', { | ||||||
|         templateUrl: 'app/features/org/partials/datasourceEdit.html', |         templateUrl: 'app/features/datasources/partials/edit.html', | ||||||
|         controller : 'DataSourceEditCtrl', |         controller : 'DataSourceEditCtrl', | ||||||
|         resolve: loadOrgBundle, |         resolve: loadOrgBundle, | ||||||
|       }) |       }) | ||||||
|  | @ -131,15 +132,17 @@ define([ | ||||||
|         templateUrl: 'app/partials/reset_password.html', |         templateUrl: 'app/partials/reset_password.html', | ||||||
|         controller : 'ResetPasswordCtrl', |         controller : 'ResetPasswordCtrl', | ||||||
|       }) |       }) | ||||||
|       .when('/plugins', { |       .when('/apps', { | ||||||
|         templateUrl: 'app/features/org/partials/plugins.html', |         templateUrl: 'app/features/apps/partials/list.html', | ||||||
|         controller: 'PluginsCtrl', |         controller: 'AppListCtrl', | ||||||
|         resolve: loadOrgBundle, |         controllerAs: 'ctrl', | ||||||
|  |         resolve: loadAppsBundle, | ||||||
|       }) |       }) | ||||||
|       .when('/plugins/edit/:type', { |       .when('/apps/edit/:appId', { | ||||||
|         templateUrl: 'app/features/org/partials/pluginEdit.html', |         templateUrl: 'app/features/apps/partials/edit.html', | ||||||
|         controller: 'PluginEditCtrl', |         controller: 'AppEditCtrl', | ||||||
|         resolve: loadOrgBundle, |         controllerAs: 'ctrl', | ||||||
|  |         resolve: loadAppsBundle, | ||||||
|       }) |       }) | ||||||
|       .when('/global-alerts', { |       .when('/global-alerts', { | ||||||
|         templateUrl: 'app/features/dashboard/partials/globalAlerts.html', |         templateUrl: 'app/features/dashboard/partials/globalAlerts.html', | ||||||
|  |  | ||||||
|  | @ -46,6 +46,10 @@ function (angular, _, coreModule) { | ||||||
|         }, timeout); |         }, timeout); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       if (!$rootScope.$$phase) { | ||||||
|  |         $rootScope.$digest(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       return(newAlert); |       return(newAlert); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ define([ | ||||||
| function (angular, _, coreModule, config) { | function (angular, _, coreModule, config) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   coreModule.default.service('datasourceSrv', function($q, $injector) { |   coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope) { | ||||||
|     var self = this; |     var self = this; | ||||||
| 
 | 
 | ||||||
|     this.init = function() { |     this.init = function() { | ||||||
|  | @ -58,18 +58,27 @@ function (angular, _, coreModule, config) { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       var deferred = $q.defer(); |       var deferred = $q.defer(); | ||||||
| 
 |  | ||||||
|       var pluginDef = dsConfig.meta; |       var pluginDef = dsConfig.meta; | ||||||
| 
 | 
 | ||||||
|       System.import(pluginDef.module).then(function() { |       System.import(pluginDef.module).then(function(plugin) { | ||||||
|         var AngularService = $injector.get(pluginDef.serviceName); |         // check if its in cache now
 | ||||||
|         var instance = new AngularService(dsConfig, pluginDef); |         if (self.datasources[name]) { | ||||||
|  |           deferred.resolve(self.datasources[name]); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // plugin module needs to export a constructor function named Datasource
 | ||||||
|  |         if (!plugin.Datasource) { | ||||||
|  |           throw "Plugin module is missing Datasource constructor"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var instance = $injector.instantiate(plugin.Datasource, {instanceSettings: dsConfig}); | ||||||
|         instance.meta = pluginDef; |         instance.meta = pluginDef; | ||||||
|         instance.name = name; |         instance.name = name; | ||||||
|         self.datasources[name] = instance; |         self.datasources[name] = instance; | ||||||
|         deferred.resolve(instance); |         deferred.resolve(instance); | ||||||
|       }).catch(function(err) { |       }).catch(function(err) { | ||||||
|         console.log('Failed to load data source: ' + err); |         $rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       return deferred.promise; |       return deferred.promise; | ||||||
|  |  | ||||||
|  | @ -44,7 +44,7 @@ | ||||||
| 				<table class="grafana-options-table"> | 				<table class="grafana-options-table"> | ||||||
| 					<tr ng-repeat="annotation in annotations"> | 					<tr ng-repeat="annotation in annotations"> | ||||||
| 						<td style="width:90%"> | 						<td style="width:90%"> | ||||||
|                             <i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>   | 							<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>   | ||||||
| 							{{annotation.name}} | 							{{annotation.name}} | ||||||
| 						</td> | 						</td> | ||||||
| 						<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td> | 						<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | import './edit_ctrl'; | ||||||
|  | import './list_ctrl'; | ||||||
|  | @ -0,0 +1,43 @@ | ||||||
|  | ///<reference path="../../headers/common.d.ts" />
 | ||||||
|  | 
 | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import angular from 'angular'; | ||||||
|  | 
 | ||||||
|  | export class AppSrv { | ||||||
|  |   apps: any = {}; | ||||||
|  | 
 | ||||||
|  |   /** @ngInject */ | ||||||
|  |   constructor( | ||||||
|  |     private $rootScope, | ||||||
|  |     private $timeout, | ||||||
|  |     private $q, | ||||||
|  |     private backendSrv) { | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get(type) { | ||||||
|  |     return this.getAll().then(() => { | ||||||
|  |       return this.apps[type]; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getAll() { | ||||||
|  |     if (!_.isEmpty(this.apps)) { | ||||||
|  |       return this.$q.when(this.apps); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return this.backendSrv.get('api/org/apps').then(results => { | ||||||
|  |       return results.reduce((prev, current) => { | ||||||
|  |         prev[current.type] = current; | ||||||
|  |         return prev; | ||||||
|  |       }, this.apps); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   update(app) { | ||||||
|  |     return this.backendSrv.post('api/org/apps', app).then(resp => { | ||||||
|  | 
 | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | angular.module('grafana.services').service('appSrv', AppSrv); | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | ///<reference path="../../headers/common.d.ts" />
 | ||||||
|  | 
 | ||||||
|  | import config from 'app/core/config'; | ||||||
|  | import angular from 'angular'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | 
 | ||||||
|  | export class AppEditCtrl { | ||||||
|  |   appModel: any; | ||||||
|  | 
 | ||||||
|  |   /** @ngInject */ | ||||||
|  |   constructor(private backendSrv: any, private $routeParams: any) {} | ||||||
|  | 
 | ||||||
|  |   init() { | ||||||
|  |     this.appModel = {}; | ||||||
|  |     this.backendSrv.get(`/api/org/apps/${this.$routeParams.appId}/settings`).then(result => { | ||||||
|  |       this.appModel = result; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   update(options) { | ||||||
|  |     var updateCmd = _.extend({ | ||||||
|  |       appId: this.appModel.appId, | ||||||
|  |       orgId: this.appModel.orgId, | ||||||
|  |       enabled: this.appModel.enabled, | ||||||
|  |       pinned: this.appModel.pinned, | ||||||
|  |       jsonData: this.appModel.jsonData, | ||||||
|  |     }, options); | ||||||
|  | 
 | ||||||
|  |     this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() { | ||||||
|  |       window.location.href = window.location.href; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toggleEnabled() { | ||||||
|  |     this.update({enabled: this.appModel.enabled}); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   togglePinned() { | ||||||
|  |     this.update({pinned: this.appModel.pinned}); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | angular.module('grafana.controllers').controller('AppEditCtrl', AppEditCtrl); | ||||||
|  | 
 | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | ///<reference path="../../headers/common.d.ts" />
 | ||||||
|  | 
 | ||||||
|  | import config = require('app/core/config'); | ||||||
|  | import angular from 'angular'; | ||||||
|  | 
 | ||||||
|  | export class AppListCtrl { | ||||||
|  |   apps: any[]; | ||||||
|  | 
 | ||||||
|  |   /** @ngInject */ | ||||||
|  |   constructor(private backendSrv: any) {} | ||||||
|  | 
 | ||||||
|  |   init() { | ||||||
|  |     this.backendSrv.get('api/org/apps').then(apps => { | ||||||
|  |       this.apps = apps; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | angular.module('grafana.controllers').controller('AppListCtrl', AppListCtrl); | ||||||
|  | @ -0,0 +1,102 @@ | ||||||
|  | <topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true"> | ||||||
|  | 	<ul class="nav"> | ||||||
|  | 		<li ><a href="apps">Overview</a></li> | ||||||
|  | 		<li class="active" ><a href="apps/edit/{{ctrl.current.type}}">Edit</a></li> | ||||||
|  | 	</ul> | ||||||
|  | </topnav> | ||||||
|  | 
 | ||||||
|  | <div class="page-container" style="background: transparent; border: 0;"> | ||||||
|  | 	<div class="apps-side-box"> | ||||||
|  | 		<div class="apps-side-box-logo" > | ||||||
|  | 			<img src="{{ctrl.appModel.info.logos.large}}"> | ||||||
|  | 		</div> | ||||||
|  | 		<ul class="app-side-box-links"> | ||||||
|  | 			<li> | ||||||
|  | 				By <a href="{{ctrl.appModel.info.author.url}}" class="external-link" target="_blank">{{ctrl.appModel.info.author.name}}</a> | ||||||
|  | 			</li> | ||||||
|  | 			<li ng-repeat="link in ctrl.appModel.info.links"> | ||||||
|  | 				<a href="{{link.url}}" class="external-link" target="_blank">{{link.name}}</a> | ||||||
|  | 			</li> | ||||||
|  | 		</ul> | ||||||
|  | 	</div> | ||||||
|  | 
 | ||||||
|  |   <div class="page-wide-margined" ng-init="ctrl.init()"> | ||||||
|  | 		<h1> | ||||||
|  | 			{{ctrl.appModel.name}} | ||||||
|  | 		</h1> | ||||||
|  | 		<em> | ||||||
|  | 			{{ctrl.appModel.info.description}}<br> | ||||||
|  | 			<span style="small"> | ||||||
|  | 			Version: {{ctrl.appModel.info.version}}     Updated: {{ctrl.appModel.info.updated}} | ||||||
|  | 		</span> | ||||||
|  | 
 | ||||||
|  | 		</em> | ||||||
|  | 		<br><br> | ||||||
|  | 
 | ||||||
|  | 		<div class="form-inline"> | ||||||
|  | 			<editor-checkbox text="Enabled" model="ctrl.appModel.enabled" change="ctrl.toggleEnabled()"></editor-checkbox> | ||||||
|  | 			      | ||||||
|  | 			<editor-checkbox text="Pinned" model="ctrl.appModel.pinned" change="ctrl.togglePinned()"></editor-checkbox> | ||||||
|  | 		</div> | ||||||
|  | 
 | ||||||
|  | 		<section class="simple-box"> | ||||||
|  | 			<h3 class="simple-box-header">Included with app:</h3> | ||||||
|  | 			<div class="flex-container"> | ||||||
|  | 				<div class="simple-box-body simple-box-column"> | ||||||
|  | 					<div class="simple-box-column-header"> | ||||||
|  | 						<i class="fa fa-th-large"></i> | ||||||
|  | 						Dashboards | ||||||
|  | 					</div> | ||||||
|  | 					<ul> | ||||||
|  | 						<li><em class="small">None</em></li> | ||||||
|  | 					</ul> | ||||||
|  | 				</div> | ||||||
|  | 				<div class="simple-box-body simple-box-column"> | ||||||
|  | 					<div class="simple-box-column-header"> | ||||||
|  | 						<i class="fa fa-line-chart"></i> | ||||||
|  | 						Panels | ||||||
|  | 					</div> | ||||||
|  | 					<ul> | ||||||
|  | 						<li><em class="small">None</em></li> | ||||||
|  | 					</ul> | ||||||
|  | 				</div> | ||||||
|  | 				<div class="simple-box-body simple-box-column"> | ||||||
|  | 					<div class="simple-box-column-header"> | ||||||
|  | 						<i class="fa fa-database"></i> | ||||||
|  | 						Datasources | ||||||
|  | 					</div> | ||||||
|  | 					<ul> | ||||||
|  | 						<li><em class="small">None</em></li> | ||||||
|  | 					</ul> | ||||||
|  | 				</div> | ||||||
|  | 				<div class="simple-box-body simple-box-column"> | ||||||
|  | 					<div class="simple-box-column-header"> | ||||||
|  | 						<i class="fa fa-files-o"></i> | ||||||
|  | 						Pages | ||||||
|  | 					</div> | ||||||
|  | 					<ul> | ||||||
|  | 						<li ng-repeat="page in ctrl.appModel.pages"> | ||||||
|  | 							<a href="{{page.url}}" class="external-link">{{page.name}}</a> | ||||||
|  | 						</li> | ||||||
|  | 					</ul> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 			</div> | ||||||
|  | 		</section> | ||||||
|  | 
 | ||||||
|  | 		<section class="simple-box"> | ||||||
|  | 			<h3 class="simple-box-header">Dependencies:</h3> | ||||||
|  | 			<div class="simple-box-body"> | ||||||
|  | 				Grafana 2.6.x | ||||||
|  | 			</div> | ||||||
|  | 		</section> | ||||||
|  | 
 | ||||||
|  | 		<section class="simple-box"> | ||||||
|  | 			<h3 class="simple-box-header">Configuration:</h3> | ||||||
|  | 			<div class="simple-box-body"> | ||||||
|  | 			</div> | ||||||
|  | 		</section> | ||||||
|  | 
 | ||||||
|  | 		<app-config-loader></app-config-loader> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | @ -0,0 +1,51 @@ | ||||||
|  | <topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true"> | ||||||
|  | 	<ul class="nav"> | ||||||
|  | 		<li class="active" ><a href="org/apps">Overview</a></li> | ||||||
|  | 	</ul> | ||||||
|  | </topnav> | ||||||
|  | 
 | ||||||
|  | <div class="page-container" style="background: transparent; border: 0;"> | ||||||
|  |   <div class="page-wide" ng-init="ctrl.init()"> | ||||||
|  |     <h2>Apps</h2> | ||||||
|  | 
 | ||||||
|  | 		<div ng-if="!ctrl.apps"> | ||||||
|  | 			<em>No apps defined</em> | ||||||
|  | 		</div> | ||||||
|  | 
 | ||||||
|  | 		<ul class="filter-list"> | ||||||
|  |       <li ng-repeat="app in ctrl.apps"> | ||||||
|  |         <ul class="filter-list-card"> | ||||||
|  | 					<li class="filter-list-card-image"> | ||||||
|  | 						<img ng-src="{{app.info.logos.small}}"> | ||||||
|  | 					</li> | ||||||
|  |           <li> | ||||||
|  |             <div class="filter-list-card-controls"> | ||||||
|  |               <div class="filter-list-card-config"> | ||||||
|  | 								<a href="apps/edit/{{app.appId}}"> | ||||||
|  | 									<i class="fa fa-cog"></i> | ||||||
|  | 								</a> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  | 						<span class="filter-list-card-title"> | ||||||
|  | 							<a href="apps/edit/{{app.appId}}"> | ||||||
|  | 								{{app.name}} | ||||||
|  | 							</a> | ||||||
|  | 							    | ||||||
|  | 							<span class="label label-info" ng-if="app.enabled"> | ||||||
|  | 								Enabled | ||||||
|  | 							</span> | ||||||
|  | 							  | ||||||
|  | 							<span class="label label-info" ng-if="app.pinned"> | ||||||
|  | 								Pinned | ||||||
|  | 							</span> | ||||||
|  | 
 | ||||||
|  | 						</span> | ||||||
|  |             <span class="filter-list-card-status"> | ||||||
|  |               <span class="filter-list-card-state">Dashboards: 1</span> | ||||||
|  |             </span> | ||||||
|  |           </li> | ||||||
|  |         </ul> | ||||||
|  |       </li> | ||||||
|  | 		</ul> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | @ -106,14 +106,6 @@ function (angular, $, config, moment) { | ||||||
|       }; |       }; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     $scope.panelEditorPath = function(type) { |  | ||||||
|       return 'app/' + config.panels[type].path + '/editor.html'; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.pulldownEditorPath = function(type) { |  | ||||||
|       return 'app/panels/'+type+'/editor.html'; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.showJsonEditor = function(evt, options) { |     $scope.showJsonEditor = function(evt, options) { | ||||||
|       var editScope = $rootScope.$new(); |       var editScope = $rootScope.$new(); | ||||||
|       editScope.object = options.object; |       editScope.object = options.object; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | define([ | ||||||
|  |   './list_ctrl', | ||||||
|  |   './edit_ctrl', | ||||||
|  | ], function () {}); | ||||||
|  | @ -9,26 +9,14 @@ function (angular, _, config) { | ||||||
|   var module = angular.module('grafana.controllers'); |   var module = angular.module('grafana.controllers'); | ||||||
|   var datasourceTypes = []; |   var datasourceTypes = []; | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceHttpSettings', function() { | ||||||
|  |     return {templateUrl: 'app/features/datasources/partials/http_settings.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) { |   module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) { | ||||||
| 
 | 
 | ||||||
|     $scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html'; |  | ||||||
| 
 |  | ||||||
|     var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}}; |     var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}}; | ||||||
| 
 | 
 | ||||||
|     $scope.indexPatternTypes = [ |  | ||||||
|       {name: 'No pattern',  value: undefined}, |  | ||||||
|       {name: 'Hourly',      value: 'Hourly',  example: '[logstash-]YYYY.MM.DD.HH'}, |  | ||||||
|       {name: 'Daily',       value: 'Daily',   example: '[logstash-]YYYY.MM.DD'}, |  | ||||||
|       {name: 'Weekly',      value: 'Weekly',  example: '[logstash-]GGGG.WW'}, |  | ||||||
|       {name: 'Monthly',     value: 'Monthly', example: '[logstash-]YYYY.MM'}, |  | ||||||
|       {name: 'Yearly',      value: 'Yearly',  example: '[logstash-]YYYY'}, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     $scope.esVersions = [ |  | ||||||
|       {name: '1.x', value: 1}, |  | ||||||
|       {name: '2.x', value: 2}, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     $scope.init = function() { |     $scope.init = function() { | ||||||
|       $scope.isNew = true; |       $scope.isNew = true; | ||||||
|       $scope.datasources = []; |       $scope.datasources = []; | ||||||
|  | @ -59,7 +47,7 @@ function (angular, _, config) { | ||||||
|       backendSrv.get('/api/datasources/' + id).then(function(ds) { |       backendSrv.get('/api/datasources/' + id).then(function(ds) { | ||||||
|         $scope.isNew = false; |         $scope.isNew = false; | ||||||
|         $scope.current = ds; |         $scope.current = ds; | ||||||
|         $scope.typeChanged(); |         return $scope.typeChanged(); | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -127,12 +115,6 @@ function (angular, _, config) { | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     $scope.indexPatternTypeChanged = function() { |  | ||||||
|       var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval}); |  | ||||||
|       $scope.current.database = def.example || 'es-index-name'; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.init(); |     $scope.init(); | ||||||
| 
 |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | @ -42,7 +42,7 @@ | ||||||
| 				<div class="clearfix"></div> | 				<div class="clearfix"></div> | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div ng-include="datasourceMeta.partials.config" ng-if="datasourceMeta.partials.config"></div> | 			<datasource-custom-settings-view ds-meta="datasourceMeta" current="current"></datasource-custom-settings-view> | ||||||
| 
 | 
 | ||||||
| 			<div ng-if="testing" style="margin-top: 25px"> | 			<div ng-if="testing" style="margin-top: 25px"> | ||||||
| 				<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5> | 				<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5> | ||||||
|  | @ -53,3 +53,5 @@ | ||||||
| 		<div class="clearfix"></div> | 		<div class="clearfix"></div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|  | 
 | ||||||
|  | <br> | ||||||
|  | @ -1,13 +1,8 @@ | ||||||
| define([ | define([ | ||||||
|   './datasourcesCtrl', |  | ||||||
|   './datasourceEditCtrl', |  | ||||||
|   './orgUsersCtrl', |   './orgUsersCtrl', | ||||||
|   './newOrgCtrl', |   './newOrgCtrl', | ||||||
|   './userInviteCtrl', |   './userInviteCtrl', | ||||||
|   './orgApiKeysCtrl', |   './orgApiKeysCtrl', | ||||||
|   './orgDetailsCtrl', |   './orgDetailsCtrl', | ||||||
|   './pluginsCtrl', |   '../datasources/all', | ||||||
|   './pluginEditCtrl', |  | ||||||
|   './plugin_srv', |  | ||||||
|   './plugin_directive', |  | ||||||
| ], function () {}); | ], function () {}); | ||||||
|  |  | ||||||
|  | @ -1,3 +0,0 @@ | ||||||
| <div> |  | ||||||
| {{current.type}} plugin does not have any additional config. |  | ||||||
| </div> |  | ||||||
|  | @ -1,42 +0,0 @@ | ||||||
| <topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true"> |  | ||||||
| 	<ul class="nav"> |  | ||||||
| 		<li ><a href="plugins">Overview</a></li> |  | ||||||
| 		<li class="active" ><a href="plugins/edit/{{current.type}}">Edit</a></li> |  | ||||||
| 	</ul> |  | ||||||
| </topnav> |  | ||||||
| 
 |  | ||||||
| <div class="page-container"> |  | ||||||
| 	<div class="page"> |  | ||||||
| 		<h2>Edit Plugin</h2> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 		<form name="editForm"> |  | ||||||
| 			<div class="tight-form"> |  | ||||||
| 				<ul class="tight-form-list"> |  | ||||||
| 					<li class="tight-form-item" style="width: 80px"> |  | ||||||
| 						Type |  | ||||||
| 					</li> |  | ||||||
| 					<li> |  | ||||||
| 						<li> |  | ||||||
| 							<input type="text" disabled="disabled" class="input-xlarge tight-form-input" ng-model="current.type"> |  | ||||||
| 						</li> |  | ||||||
| 					</li> |  | ||||||
| 					<li class="tight-form-item"> |  | ||||||
| 						Default  |  | ||||||
| 						<input class="cr1" id="current.enabled" type="checkbox" ng-model="current.enabled" ng-checked="current.enabled"> |  | ||||||
| 						<label for="current.enabled" class="cr1"></label> |  | ||||||
| 					</li> |  | ||||||
| 				</ul> |  | ||||||
| 				<div class="clearfix"></div> |  | ||||||
| 			</div> |  | ||||||
| 			<br> |  | ||||||
| 			<plugin-config-loader plugin="current"></plugin-config-loader> |  | ||||||
| 			<div class="pull-right" style="margin-top: 35px"> |  | ||||||
| 				<button type="submit" class="btn btn-success" ng-click="update()">Save</button> |  | ||||||
| 				<a class="btn btn-inverse" href="plugins">Cancel</a> |  | ||||||
| 			</div> |  | ||||||
| 			<br> |  | ||||||
| 		</form> |  | ||||||
| 
 |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
|  | @ -1,41 +0,0 @@ | ||||||
| <topnav title="Plugins" icon="fa fa-fw fa-cubes" subnav="true"> |  | ||||||
| 	<ul class="nav"> |  | ||||||
| 		<li class="active" ><a href="plugins">Overview</a></li> |  | ||||||
| 	</ul> |  | ||||||
| </topnav> |  | ||||||
| 
 |  | ||||||
| <div class="page-container"> |  | ||||||
| 	<div class="page"> |  | ||||||
| 		<h2>Plugins</h2> |  | ||||||
| 
 |  | ||||||
| 		<div ng-if="!plugins"> |  | ||||||
| 			<em>No plugins defined</em> |  | ||||||
| 		</div> |  | ||||||
| 
 |  | ||||||
| 		<table class="grafana-options-table" ng-if="plugins"> |  | ||||||
| 			<tr> |  | ||||||
| 				<td><strong>Type</strong></td> |  | ||||||
| 				<td></td> |  | ||||||
| 				<td></td> |  | ||||||
| 			</tr> |  | ||||||
| 			<tr ng-repeat="(type, p) in plugins"> |  | ||||||
| 				<td style="width:1%"> |  | ||||||
| 					<i class="fa fa-cubes"></i>   |  | ||||||
| 					{{p.type}} |  | ||||||
| 				</td> |  | ||||||
| 				<td style="width: 1%"> |  | ||||||
| 					<a href="plugins/edit/{{p.type}}" class="btn btn-inverse btn-mini"> |  | ||||||
| 						<i class="fa fa-edit"></i> |  | ||||||
| 						Edit |  | ||||||
| 					</a> |  | ||||||
| 				</td> |  | ||||||
| 				<td style="width: 1%"> |  | ||||||
| 					Enabled  |  | ||||||
| 					<input  id="p.enabled" type="checkbox" ng-model="p.enabled" ng-checked="p.enabled" ng-change="update(p)"> |  | ||||||
| 					<label for="p.enabled"></label> |  | ||||||
| 				</td> |  | ||||||
| 			</tr> |  | ||||||
| 		</table> |  | ||||||
| 
 |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
|   'lodash', |  | ||||||
|   'app/core/config', |  | ||||||
| ], |  | ||||||
| function (angular, _, config) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.controllers'); |  | ||||||
| 
 |  | ||||||
|   module.controller('PluginEditCtrl', function($scope, pluginSrv, $routeParams) { |  | ||||||
|     $scope.init = function() { |  | ||||||
|       $scope.current = {}; |  | ||||||
|       $scope.getPlugins(); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.getPlugins = function() { |  | ||||||
|       pluginSrv.get($routeParams.type).then(function(result) { |  | ||||||
|         $scope.current = _.clone(result); |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.update = function() { |  | ||||||
|       $scope._update(); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope._update = function() { |  | ||||||
|       pluginSrv.update($scope.current).then(function() { |  | ||||||
|         window.location.href = config.appSubUrl + "plugins"; |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.init(); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  | @ -1,47 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
| ], |  | ||||||
| function (angular) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.directives'); |  | ||||||
| 
 |  | ||||||
|   module.directive('pluginConfigLoader', function($compile) { |  | ||||||
|     return { |  | ||||||
|       restrict: 'E', |  | ||||||
|       link: function(scope, elem) { |  | ||||||
|         var directive = 'grafana-plugin-core'; |  | ||||||
|         //wait for the parent scope to be applied.
 |  | ||||||
|         scope.$watch("current", function(newVal) { |  | ||||||
|           if (newVal) { |  | ||||||
|             if (newVal.module) { |  | ||||||
|               directive = 'grafana-plugin-'+newVal.type; |  | ||||||
|             } |  | ||||||
|             scope.require([newVal.module], function () { |  | ||||||
|               var panelEl = angular.element(document.createElement(directive)); |  | ||||||
|               elem.append(panelEl); |  | ||||||
|               $compile(panelEl)(scope); |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   module.directive('grafanaPluginCore', function() { |  | ||||||
|     return { |  | ||||||
|       restrict: 'E', |  | ||||||
|       templateUrl: 'app/features/org/partials/pluginConfigCore.html', |  | ||||||
|       transclude: true, |  | ||||||
|       link: function(scope) { |  | ||||||
|         scope.update = function() { |  | ||||||
|           //Perform custom save events to the plugins own backend if needed.
 |  | ||||||
| 
 |  | ||||||
|           // call parent update to commit the change to the plugin object.
 |  | ||||||
|           // this will cause the page to reload.
 |  | ||||||
|           scope._update(); |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  | @ -1,58 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
|   'lodash', |  | ||||||
| ], |  | ||||||
| function (angular, _) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.services'); |  | ||||||
| 
 |  | ||||||
|   module.service('pluginSrv', function($rootScope, $timeout, $q, backendSrv) { |  | ||||||
|     var self = this; |  | ||||||
|     this.init = function() { |  | ||||||
|       console.log("pluginSrv init"); |  | ||||||
|       this.plugins = {}; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     this.get = function(type) { |  | ||||||
|       return $q(function(resolve) { |  | ||||||
|         if (type in self.plugins) { |  | ||||||
|           return resolve(self.plugins[type]); |  | ||||||
|         } |  | ||||||
|         backendSrv.get('/api/plugins').then(function(results) { |  | ||||||
|           _.forEach(results, function(p) { |  | ||||||
|             self.plugins[p.type] = p; |  | ||||||
|           }); |  | ||||||
|           return resolve(self.plugins[type]); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     this.getAll = function() { |  | ||||||
|       return $q(function(resolve) { |  | ||||||
|         if (!_.isEmpty(self.plugins)) { |  | ||||||
|           return resolve(self.plugins); |  | ||||||
|         } |  | ||||||
|         backendSrv.get('api/plugins').then(function(results) { |  | ||||||
|           _.forEach(results, function(p) { |  | ||||||
|             self.plugins[p.type] = p; |  | ||||||
|           }); |  | ||||||
|           return resolve(self.plugins); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     this.update = function(plugin) { |  | ||||||
|       return $q(function(resolve, reject) { |  | ||||||
|         backendSrv.post('/api/plugins', plugin).then(function(resp) { |  | ||||||
|           self.plugins[plugin.type] = plugin; |  | ||||||
|           resolve(resp); |  | ||||||
|         }, function(resp) { |  | ||||||
|           reject(resp); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     this.init(); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  | @ -1,33 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
|   'app/core/config', |  | ||||||
| ], |  | ||||||
| function (angular, config) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.controllers'); |  | ||||||
| 
 |  | ||||||
|   module.controller('PluginsCtrl', function($scope, $location, pluginSrv) { |  | ||||||
| 
 |  | ||||||
|     $scope.init = function() { |  | ||||||
|       $scope.plugins = {}; |  | ||||||
|       $scope.getPlugins(); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.getPlugins = function() { |  | ||||||
|       pluginSrv.getAll().then(function(result) { |  | ||||||
|         console.log(result); |  | ||||||
|         $scope.plugins = result; |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.update = function(plugin) { |  | ||||||
|       pluginSrv.update(plugin).then(function() { |  | ||||||
|         window.location.href = config.appSubUrl + $location.path(); |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     $scope.init(); |  | ||||||
| 
 |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
|  | @ -43,6 +43,33 @@ function (angular, $, config) { | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsView', function($compile) { | ||||||
|  |     return { | ||||||
|  |       restrict: 'E', | ||||||
|  |       scope: { | ||||||
|  |         dsMeta: "=", | ||||||
|  |         current: "=", | ||||||
|  |       }, | ||||||
|  |       link: function(scope, elem) { | ||||||
|  |         scope.$watch("dsMeta.module", function() { | ||||||
|  |           if (!scope.dsMeta) { | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           System.import(scope.dsMeta.module).then(function() { | ||||||
|  |             elem.empty(); | ||||||
|  |             var panelEl = angular.element(document.createElement('datasource-custom-settings-view-' + scope.dsMeta.id)); | ||||||
|  |             elem.append(panelEl); | ||||||
|  |             $compile(panelEl)(scope); | ||||||
|  |           }).catch(function(err) { | ||||||
|  |             console.log('Failed to load plugin:', err); | ||||||
|  |             scope.appEvent('alert-error', ['Plugin Load Error', 'Failed to load plugin ' + scope.dsMeta.id + ', ' + err]); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   module.service('dynamicDirectiveSrv', function($compile, $parse, datasourceSrv) { |   module.service('dynamicDirectiveSrv', function($compile, $parse, datasourceSrv) { | ||||||
|     var self = this; |     var self = this; | ||||||
| 
 | 
 | ||||||
|  | @ -62,12 +89,26 @@ function (angular, $, config) { | ||||||
| 
 | 
 | ||||||
|         editorScope = options.scope.$new(); |         editorScope = options.scope.$new(); | ||||||
|         datasourceSrv.get(newVal).then(function(ds) { |         datasourceSrv.get(newVal).then(function(ds) { | ||||||
|           self.addDirective(options, ds.meta.type, editorScope); |           self.addDirective(options, ds.meta.id, editorScope); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceEditorView', function(dynamicDirectiveSrv) { | ||||||
|  |     return { | ||||||
|  |       restrict: 'E', | ||||||
|  |       link: function(scope, elem, attrs) { | ||||||
|  |         dynamicDirectiveSrv.define({ | ||||||
|  |           datasourceProperty: attrs.datasource, | ||||||
|  |           name: attrs.name, | ||||||
|  |           scope: scope, | ||||||
|  |           parentElem: elem, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   module.directive('queryEditorLoader', function($compile, $parse, datasourceSrv) { |   module.directive('queryEditorLoader', function($compile, $parse, datasourceSrv) { | ||||||
|     return { |     return { | ||||||
|       restrict: 'E', |       restrict: 'E', | ||||||
|  | @ -90,7 +131,7 @@ function (angular, $, config) { | ||||||
|               scope.target.refId = 'A'; |               scope.target.refId = 'A'; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.type)); |             var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.id)); | ||||||
|             elem.append(panelEl); |             elem.append(panelEl); | ||||||
|             $compile(panelEl)(editorScope); |             $compile(panelEl)(editorScope); | ||||||
|           }); |           }); | ||||||
|  | @ -99,20 +140,6 @@ function (angular, $, config) { | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   module.directive('datasourceEditorView', function(dynamicDirectiveSrv) { |  | ||||||
|     return { |  | ||||||
|       restrict: 'E', |  | ||||||
|       link: function(scope, elem, attrs) { |  | ||||||
|         dynamicDirectiveSrv.define({ |  | ||||||
|           datasourceProperty: attrs.datasource, |  | ||||||
|           name: attrs.name, |  | ||||||
|           scope: scope, |  | ||||||
|           parentElem: elem, |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   module.directive('panelResizer', function($rootScope) { |   module.directive('panelResizer', function($rootScope) { | ||||||
|     return { |     return { | ||||||
|       restrict: 'E', |       restrict: 'E', | ||||||
|  |  | ||||||
|  | @ -34,13 +34,6 @@ | ||||||
| 			</ul> | 			</ul> | ||||||
| 		</li> | 		</li> | ||||||
| 
 | 
 | ||||||
| 		<li ng-if="!contextSrv.isSignedIn"> |  | ||||||
| 			<a href="login" class="sidemenu-item" target="_self"> |  | ||||||
| 				<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-sign-in"></i></span> |  | ||||||
| 				<span class="sidemenu-item-text">Sign in</span> |  | ||||||
| 			</a> |  | ||||||
| 		</li> |  | ||||||
| 
 |  | ||||||
| 		<li class="sidemenu-system-section" ng-if="systemSection"> | 		<li class="sidemenu-system-section" ng-if="systemSection"> | ||||||
| 			<div class="sidemenu-system-section-inner"> | 			<div class="sidemenu-system-section-inner"> | ||||||
| 				<i class="fa fa-fw fa-cubes"></i> | 				<i class="fa fa-fw fa-cubes"></i> | ||||||
|  | @ -52,8 +45,11 @@ | ||||||
| 		</li> | 		</li> | ||||||
| 
 | 
 | ||||||
| 		<li ng-repeat="item in mainLinks"> | 		<li ng-repeat="item in mainLinks"> | ||||||
| 			<a href="{{item.href}}" class="sidemenu-item sidemenu-main-link" target="{{item.target}}"> | 			<a href="{{item.url}}" class="sidemenu-item sidemenu-main-link" target="{{item.target}}"> | ||||||
| 				<span class="icon-circle sidemenu-icon"><i class="{{item.icon}}"></i></span> | 				<span class="icon-circle sidemenu-icon"> | ||||||
|  | 					<i class="{{item.icon}}" ng-show="item.icon"></i> | ||||||
|  | 					<img ng-src="{{item.img}}" ng-show="item.img"> | ||||||
|  | 				</span> | ||||||
| 				<span class="sidemenu-item-text">{{item.text}}</span> | 				<span class="sidemenu-item-text">{{item.text}}</span> | ||||||
| 			</a> | 			</a> | ||||||
| 		</li> | 		</li> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | declare var Datasource: any; | ||||||
|  | export default Datasource; | ||||||
|  | 
 | ||||||
|  | @ -4,24 +4,19 @@ define([ | ||||||
|   'moment', |   'moment', | ||||||
|   'app/core/utils/datemath', |   'app/core/utils/datemath', | ||||||
|   './query_ctrl', |   './query_ctrl', | ||||||
|   './directives', |  | ||||||
| ], | ], | ||||||
| function (angular, _, moment, dateMath) { | function (angular, _, moment, dateMath) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.services'); |   /** @ngInject */ | ||||||
|  |   function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv) { | ||||||
|  |     this.type = 'cloudwatch'; | ||||||
|  |     this.name = instanceSettings.name; | ||||||
|  |     this.supportMetrics = true; | ||||||
|  |     this.proxyUrl = instanceSettings.url; | ||||||
|  |     this.defaultRegion = instanceSettings.jsonData.defaultRegion; | ||||||
| 
 | 
 | ||||||
|   module.factory('CloudWatchDatasource', function($q, backendSrv, templateSrv) { |     this.query = function(options) { | ||||||
| 
 |  | ||||||
|     function CloudWatchDatasource(datasource) { |  | ||||||
|       this.type = 'cloudwatch'; |  | ||||||
|       this.name = datasource.name; |  | ||||||
|       this.supportMetrics = true; |  | ||||||
|       this.proxyUrl = datasource.url; |  | ||||||
|       this.defaultRegion = datasource.jsonData.defaultRegion; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     CloudWatchDatasource.prototype.query = function(options) { |  | ||||||
|       var start = convertToCloudWatchTime(options.range.from, false); |       var start = convertToCloudWatchTime(options.range.from, false); | ||||||
|       var end = convertToCloudWatchTime(options.range.to, true); |       var end = convertToCloudWatchTime(options.range.to, true); | ||||||
| 
 | 
 | ||||||
|  | @ -72,7 +67,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.performTimeSeriesQuery = function(query, start, end) { |     this.performTimeSeriesQuery = function(query, start, end) { | ||||||
|       return this.awsRequest({ |       return this.awsRequest({ | ||||||
|         region: query.region, |         region: query.region, | ||||||
|         action: 'GetMetricStatistics', |         action: 'GetMetricStatistics', | ||||||
|  | @ -88,15 +83,15 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.getRegions = function() { |     this.getRegions = function() { | ||||||
|       return this.awsRequest({action: '__GetRegions'}); |       return this.awsRequest({action: '__GetRegions'}); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.getNamespaces = function() { |     this.getNamespaces = function() { | ||||||
|       return this.awsRequest({action: '__GetNamespaces'}); |       return this.awsRequest({action: '__GetNamespaces'}); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.getMetrics = function(namespace) { |     this.getMetrics = function(namespace) { | ||||||
|       return this.awsRequest({ |       return this.awsRequest({ | ||||||
|         action: '__GetMetrics', |         action: '__GetMetrics', | ||||||
|         parameters: { |         parameters: { | ||||||
|  | @ -105,7 +100,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.getDimensionKeys = function(namespace) { |     this.getDimensionKeys = function(namespace) { | ||||||
|       return this.awsRequest({ |       return this.awsRequest({ | ||||||
|         action: '__GetDimensions', |         action: '__GetDimensions', | ||||||
|         parameters: { |         parameters: { | ||||||
|  | @ -114,7 +109,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) { |     this.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) { | ||||||
|       var request = { |       var request = { | ||||||
|         region: templateSrv.replace(region), |         region: templateSrv.replace(region), | ||||||
|         action: 'ListMetrics', |         action: 'ListMetrics', | ||||||
|  | @ -141,7 +136,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.performEC2DescribeInstances = function(region, filters, instanceIds) { |     this.performEC2DescribeInstances = function(region, filters, instanceIds) { | ||||||
|       return this.awsRequest({ |       return this.awsRequest({ | ||||||
|         region: region, |         region: region, | ||||||
|         action: 'DescribeInstances', |         action: 'DescribeInstances', | ||||||
|  | @ -149,7 +144,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.metricFindQuery = function(query) { |     this.metricFindQuery = function(query) { | ||||||
|       var region; |       var region; | ||||||
|       var namespace; |       var namespace; | ||||||
|       var metricName; |       var metricName; | ||||||
|  | @ -210,7 +205,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       return $q.when([]); |       return $q.when([]); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) { |     this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) { | ||||||
|       return this.awsRequest({ |       return this.awsRequest({ | ||||||
|         region: region, |         region: region, | ||||||
|         action: 'DescribeAlarmsForMetric', |         action: 'DescribeAlarmsForMetric', | ||||||
|  | @ -218,7 +213,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) { |     this.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) { | ||||||
|       return this.awsRequest({ |       return this.awsRequest({ | ||||||
|         region: region, |         region: region, | ||||||
|         action: 'DescribeAlarmHistory', |         action: 'DescribeAlarmHistory', | ||||||
|  | @ -226,7 +221,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.annotationQuery = function(options) { |     this.annotationQuery = function(options) { | ||||||
|       var annotation = options.annotation; |       var annotation = options.annotation; | ||||||
|       var region = templateSrv.replace(annotation.region); |       var region = templateSrv.replace(annotation.region); | ||||||
|       var namespace = templateSrv.replace(annotation.namespace); |       var namespace = templateSrv.replace(annotation.namespace); | ||||||
|  | @ -278,7 +273,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       return d.promise; |       return d.promise; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.testDatasource = function() { |     this.testDatasource = function() { | ||||||
|       /* use billing metrics for test */ |       /* use billing metrics for test */ | ||||||
|       var region = this.defaultRegion; |       var region = this.defaultRegion; | ||||||
|       var namespace = 'AWS/Billing'; |       var namespace = 'AWS/Billing'; | ||||||
|  | @ -290,7 +285,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.awsRequest = function(data) { |     this.awsRequest = function(data) { | ||||||
|       var options = { |       var options = { | ||||||
|         method: 'POST', |         method: 'POST', | ||||||
|         url: this.proxyUrl, |         url: this.proxyUrl, | ||||||
|  | @ -302,7 +297,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     CloudWatchDatasource.prototype.getDefaultRegion = function() { |     this.getDefaultRegion = function() { | ||||||
|       return this.defaultRegion; |       return this.defaultRegion; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -361,7 +356,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return CloudWatchDatasource; |   } | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|  |   return CloudWatchDatasource; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| define([ | define([ | ||||||
|   'angular', |   'angular', | ||||||
|  |   './datasource', | ||||||
|   './query_parameter_ctrl', |   './query_parameter_ctrl', | ||||||
| ], | ], | ||||||
| function (angular) { | function (angular, CloudWatchDatasource) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.directives'); |   var module = angular.module('grafana.directives'); | ||||||
|  | @ -28,4 +29,11 @@ function (angular) { | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsViewCloudwatch', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/edit_view.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return  { | ||||||
|  |     Datasource: CloudWatchDatasource | ||||||
|  |   }; | ||||||
| }); | }); | ||||||
|  | @ -1,16 +1,7 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "CloudWatch", |   "name": "CloudWatch", | ||||||
| 
 |   "id": "cloudwatch", | ||||||
|   "type": "cloudwatch", |  | ||||||
|   "serviceName": "CloudWatchDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/cloudwatch/datasource", |  | ||||||
| 
 |  | ||||||
|   "partials": { |  | ||||||
|     "config": "app/plugins/datasource/cloudwatch/partials/config.html", |  | ||||||
|     "query": "app/plugins/datasource/cloudwatch/partials/query.editor.html" |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   "metrics": true, |   "metrics": true, | ||||||
|   "annotations": true |   "annotations": true | ||||||
|  |  | ||||||
|  | @ -3,25 +3,25 @@ import "../datasource"; | ||||||
| import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | ||||||
| import moment from 'moment'; | import moment from 'moment'; | ||||||
| import helpers from 'test/specs/helpers'; | import helpers from 'test/specs/helpers'; | ||||||
|  | import Datasource from "../datasource"; | ||||||
| 
 | 
 | ||||||
| describe('CloudWatchDatasource', function() { | describe('CloudWatchDatasource', function() { | ||||||
|   var ctx = new helpers.ServiceTestContext(); |   var ctx = new helpers.ServiceTestContext(); | ||||||
|  |   var instanceSettings = { | ||||||
|  |     jsonData: {defaultRegion: 'us-east-1', access: 'proxy'}, | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|   beforeEach(angularMocks.module('grafana.core')); |   beforeEach(angularMocks.module('grafana.core')); | ||||||
|   beforeEach(angularMocks.module('grafana.services')); |   beforeEach(angularMocks.module('grafana.services')); | ||||||
|   beforeEach(angularMocks.module('grafana.controllers')); |   beforeEach(angularMocks.module('grafana.controllers')); | ||||||
| 
 |  | ||||||
|   beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); |   beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); | ||||||
|   beforeEach(ctx.createService('CloudWatchDatasource')); |  | ||||||
| 
 | 
 | ||||||
|   beforeEach(function() { |   beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { | ||||||
|     ctx.ds = new ctx.service({ |     ctx.$q = $q; | ||||||
|       jsonData: { |     ctx.$httpBackend =  $httpBackend; | ||||||
|         defaultRegion: 'us-east-1', |     ctx.$rootScope = $rootScope; | ||||||
|         access: 'proxy' |     ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings}); | ||||||
|       } |   })); | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|   describe('When performing CloudWatch query', function() { |   describe('When performing CloudWatch query', function() { | ||||||
|     var requestParams; |     var requestParams; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | declare var Datasource: any; | ||||||
|  | export default Datasource; | ||||||
|  | 
 | ||||||
|  | @ -7,33 +7,27 @@ define([ | ||||||
|   './index_pattern', |   './index_pattern', | ||||||
|   './elastic_response', |   './elastic_response', | ||||||
|   './query_ctrl', |   './query_ctrl', | ||||||
|   './directives' |  | ||||||
| ], | ], | ||||||
| function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) { | function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.services'); |   /** @ngInject */ | ||||||
|  |   function ElasticDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) { | ||||||
|  |     this.basicAuth = instanceSettings.basicAuth; | ||||||
|  |     this.withCredentials = instanceSettings.withCredentials; | ||||||
|  |     this.url = instanceSettings.url; | ||||||
|  |     this.name = instanceSettings.name; | ||||||
|  |     this.index = instanceSettings.index; | ||||||
|  |     this.timeField = instanceSettings.jsonData.timeField; | ||||||
|  |     this.esVersion = instanceSettings.jsonData.esVersion; | ||||||
|  |     this.indexPattern = new IndexPattern(instanceSettings.index, instanceSettings.jsonData.interval); | ||||||
|  |     this.interval = instanceSettings.jsonData.timeInterval; | ||||||
|  |     this.queryBuilder = new ElasticQueryBuilder({ | ||||||
|  |       timeField: this.timeField, | ||||||
|  |       esVersion: this.esVersion, | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|   module.factory('ElasticDatasource', function($q, backendSrv, templateSrv, timeSrv) { |     this._request = function(method, url, data) { | ||||||
| 
 |  | ||||||
|     function ElasticDatasource(datasource) { |  | ||||||
|       this.type = 'elasticsearch'; |  | ||||||
|       this.basicAuth = datasource.basicAuth; |  | ||||||
|       this.withCredentials = datasource.withCredentials; |  | ||||||
|       this.url = datasource.url; |  | ||||||
|       this.name = datasource.name; |  | ||||||
|       this.index = datasource.index; |  | ||||||
|       this.timeField = datasource.jsonData.timeField; |  | ||||||
|       this.esVersion = datasource.jsonData.esVersion; |  | ||||||
|       this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval); |  | ||||||
|       this.interval = datasource.jsonData.timeInterval; |  | ||||||
|       this.queryBuilder = new ElasticQueryBuilder({ |  | ||||||
|         timeField: this.timeField, |  | ||||||
|         esVersion: this.esVersion, |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     ElasticDatasource.prototype._request = function(method, url, data) { |  | ||||||
|       var options = { |       var options = { | ||||||
|         url: this.url + "/" + url, |         url: this.url + "/" + url, | ||||||
|         method: method, |         method: method, | ||||||
|  | @ -52,21 +46,21 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       return backendSrv.datasourceRequest(options); |       return backendSrv.datasourceRequest(options); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype._get = function(url) { |     this._get = function(url) { | ||||||
|       return this._request('GET', this.indexPattern.getIndexForToday() + url) |       return this._request('GET', this.indexPattern.getIndexForToday() + url) | ||||||
|         .then(function(results) { |       .then(function(results) { | ||||||
|           return results.data; |         return results.data; | ||||||
|         }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype._post = function(url, data) { |     this._post = function(url, data) { | ||||||
|       return this._request('POST', url, data) |       return this._request('POST', url, data) | ||||||
|         .then(function(results) { |       .then(function(results) { | ||||||
|           return results.data; |         return results.data; | ||||||
|         }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.annotationQuery = function(options) { |     this.annotationQuery = function(options) { | ||||||
|       var annotation = options.annotation; |       var annotation = options.annotation; | ||||||
|       var timeField = annotation.timeField || '@timestamp'; |       var timeField = annotation.timeField || '@timestamp'; | ||||||
|       var queryString = annotation.query || '*'; |       var queryString = annotation.query || '*'; | ||||||
|  | @ -147,7 +141,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.testDatasource = function() { |     this.testDatasource = function() { | ||||||
|       return this._get('/_stats').then(function() { |       return this._get('/_stats').then(function() { | ||||||
|         return { status: "success", message: "Data source is working", title: "Success" }; |         return { status: "success", message: "Data source is working", title: "Success" }; | ||||||
|       }, function(err) { |       }, function(err) { | ||||||
|  | @ -159,13 +153,13 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.getQueryHeader = function(searchType, timeFrom, timeTo) { |     this.getQueryHeader = function(searchType, timeFrom, timeTo) { | ||||||
|       var header = {search_type: searchType, "ignore_unavailable": true}; |       var header = {search_type: searchType, "ignore_unavailable": true}; | ||||||
|       header.index = this.indexPattern.getIndexList(timeFrom, timeTo); |       header.index = this.indexPattern.getIndexList(timeFrom, timeTo); | ||||||
|       return angular.toJson(header); |       return angular.toJson(header); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.query = function(options) { |     this.query = function(options) { | ||||||
|       var payload = ""; |       var payload = ""; | ||||||
|       var target; |       var target; | ||||||
|       var sentTargets = []; |       var sentTargets = []; | ||||||
|  | @ -203,7 +197,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.getFields = function(query) { |     this.getFields = function(query) { | ||||||
|       return this._get('/_mapping').then(function(res) { |       return this._get('/_mapping').then(function(res) { | ||||||
|         var fields = {}; |         var fields = {}; | ||||||
|         var typeMap = { |         var typeMap = { | ||||||
|  | @ -240,7 +234,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.getTerms = function(queryDef) { |     this.getTerms = function(queryDef) { | ||||||
|       var range = timeSrv.timeRange(); |       var range = timeSrv.timeRange(); | ||||||
|       var header = this.getQueryHeader('count', range.from, range.to); |       var header = this.getQueryHeader('count', range.from, range.to); | ||||||
|       var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef)); |       var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef)); | ||||||
|  | @ -258,7 +252,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.metricFindQuery = function(query) { |     this.metricFindQuery = function(query) { | ||||||
|       query = templateSrv.replace(query); |       query = templateSrv.replace(query); | ||||||
|       query = angular.fromJson(query); |       query = angular.fromJson(query); | ||||||
|       if (!query) { |       if (!query) { | ||||||
|  | @ -273,14 +267,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.getDashboard = function(id) { |     this.getDashboard = function(id) { | ||||||
|       return this._get('/dashboard/' + id) |       return this._get('/dashboard/' + id) | ||||||
|       .then(function(result) { |       .then(function(result) { | ||||||
|         return angular.fromJson(result._source.dashboard); |         return angular.fromJson(result._source.dashboard); | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     ElasticDatasource.prototype.searchDashboards = function() { |     this.searchDashboards = function() { | ||||||
|       var query = { |       var query = { | ||||||
|         query: { query_string: { query: '*' } }, |         query: { query_string: { query: '*' } }, | ||||||
|         size: 10000, |         size: 10000, | ||||||
|  | @ -308,7 +302,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes | ||||||
|         return displayHits; |         return displayHits; | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     return ElasticDatasource; |   return ElasticDatasource; | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -20,6 +20,10 @@ function (angular) { | ||||||
|     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'}; |     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'}; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('elastic', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/config.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   module.directive('elasticMetricAgg', function() { |   module.directive('elasticMetricAgg', function() { | ||||||
|     return { |     return { | ||||||
|       templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html', |       templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html', | ||||||
|  |  | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | ///<reference path="../../../headers/common.d.ts" />
 | ||||||
|  | 
 | ||||||
|  | import angular from 'angular'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | 
 | ||||||
|  | export class EditViewCtrl { | ||||||
|  | 
 | ||||||
|  |   constructor($scope) { | ||||||
|  |     $scope.indexPatternTypes = [ | ||||||
|  |       {name: 'No pattern',  value: undefined}, | ||||||
|  |       {name: 'Hourly',      value: 'Hourly',  example: '[logstash-]YYYY.MM.DD.HH'}, | ||||||
|  |       {name: 'Daily',       value: 'Daily',   example: '[logstash-]YYYY.MM.DD'}, | ||||||
|  |       {name: 'Weekly',      value: 'Weekly',  example: '[logstash-]GGGG.WW'}, | ||||||
|  |       {name: 'Monthly',     value: 'Monthly', example: '[logstash-]YYYY.MM'}, | ||||||
|  |       {name: 'Yearly',      value: 'Yearly',  example: '[logstash-]YYYY'}, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     $scope.esVersions = [ | ||||||
|  |       {name: '1.x', value: 1}, | ||||||
|  |       {name: '2.x', value: 2}, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     $scope.indexPatternTypeChanged = function() { | ||||||
|  |       var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval}); | ||||||
|  |       $scope.current.database = def.example || 'es-index-name'; | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function editViewDirective() { | ||||||
|  |   return { | ||||||
|  |     templateUrl: 'app/plugins/datasource/elasticsearch/partials/edit_view.html', | ||||||
|  |     controller: EditViewCtrl, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export default editViewDirective; | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | define([ | ||||||
|  |   'angular', | ||||||
|  |   './datasource', | ||||||
|  |   './edit_view', | ||||||
|  |   './bucket_agg', | ||||||
|  |   './metric_agg', | ||||||
|  | ], | ||||||
|  | function (angular, ElasticDatasource, editView) { | ||||||
|  |   'use strict'; | ||||||
|  | 
 | ||||||
|  |   var module = angular.module('grafana.directives'); | ||||||
|  | 
 | ||||||
|  |   module.directive('metricQueryEditorElasticsearch', function() { | ||||||
|  |     return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   module.directive('metricQueryOptionsElasticsearch', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   module.directive('annotationsQueryEditorElasticsearch', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   module.directive('elasticMetricAgg', function() { | ||||||
|  |     return { | ||||||
|  |       templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html', | ||||||
|  |       controller: 'ElasticMetricAggCtrl', | ||||||
|  |       restrict: 'E', | ||||||
|  |       scope: { | ||||||
|  |         target: "=", | ||||||
|  |         index: "=", | ||||||
|  |         onChange: "&", | ||||||
|  |         getFields: "&", | ||||||
|  |         esVersion: '=' | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   module.directive('elasticBucketAgg', function() { | ||||||
|  |     return { | ||||||
|  |       templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html', | ||||||
|  |       controller: 'ElasticBucketAggCtrl', | ||||||
|  |       restrict: 'E', | ||||||
|  |       scope: { | ||||||
|  |         target: "=", | ||||||
|  |         index: "=", | ||||||
|  |         onChange: "&", | ||||||
|  |         getFields: "&", | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsViewElasticsearch', editView.default); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     Datasource: ElasticDatasource, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  | }); | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| <div ng-include="httpConfigPartialSrc"></div> | <datasource-http-settings></datasource-http-settings> | ||||||
| <br> |  | ||||||
| 
 | 
 | ||||||
| <h5>Elasticsearch details</h5> | <h5>Elasticsearch details</h5> | ||||||
| 
 | 
 | ||||||
|  | @ -42,8 +41,8 @@ | ||||||
| 	</ul> | 	</ul> | ||||||
| 	<div class="clearfix"></div> | 	<div class="clearfix"></div> | ||||||
| </div> | </div> | ||||||
| </div> |  | ||||||
| 
 | 
 | ||||||
|  | <br> | ||||||
| <h5>Default query settings</h5> | <h5>Default query settings</h5> | ||||||
| 
 | 
 | ||||||
| <div class="tight-form last"> | <div class="tight-form last"> | ||||||
|  | @ -53,7 +52,7 @@ | ||||||
| 		</li> | 		</li> | ||||||
| 		<li> | 		<li> | ||||||
| 			<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="current.jsonData.timeInterval" | 			<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="current.jsonData.timeInterval" | ||||||
| 						 spellcheck='false' placeholder="example: >10s"> | 			spellcheck='false' placeholder="example: >10s"> | ||||||
| 		</li> | 		</li> | ||||||
| 		<li class="tight-form-item"> | 		<li class="tight-form-item"> | ||||||
| 			<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >10s'" data-placement="right"></i> | 			<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >10s'" data-placement="right"></i> | ||||||
|  | @ -1,16 +1,7 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "Elasticsearch", |   "name": "Elasticsearch", | ||||||
| 
 |   "id": "elasticsearch", | ||||||
|   "type": "elasticsearch", |  | ||||||
|   "serviceName": "ElasticDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/elasticsearch/datasource", |  | ||||||
| 
 |  | ||||||
|   "partials": { |  | ||||||
|     "config": "app/plugins/datasource/elasticsearch/partials/config.html", |  | ||||||
|     "annotations": "app/plugins/datasource/elasticsearch/partials/annotations.editor.html" |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   "defaultMatchFormat": "lucene", |   "defaultMatchFormat": "lucene", | ||||||
|   "annotations": true, |   "annotations": true, | ||||||
|  |  | ||||||
|  | @ -1,28 +1,32 @@ | ||||||
| 
 | 
 | ||||||
| import "../datasource"; |  | ||||||
| import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | ||||||
| import moment from 'moment'; | import moment from 'moment'; | ||||||
| import angular from 'angular'; | import angular from 'angular'; | ||||||
| import helpers from 'test/specs/helpers'; | import helpers from 'test/specs/helpers'; | ||||||
|  | import Datasource from "../datasource"; | ||||||
| 
 | 
 | ||||||
| describe('ElasticDatasource', function() { | describe('ElasticDatasource', function() { | ||||||
|   var ctx = new helpers.ServiceTestContext(); |   var ctx = new helpers.ServiceTestContext(); | ||||||
|  |   var instanceSettings: any = {jsonData: {}}; | ||||||
| 
 | 
 | ||||||
|   beforeEach(angularMocks.module('grafana.core')); |   beforeEach(angularMocks.module('grafana.core')); | ||||||
|   beforeEach(angularMocks.module('grafana.services')); |   beforeEach(angularMocks.module('grafana.services')); | ||||||
|   beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); |   beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); | ||||||
|   beforeEach(ctx.createService('ElasticDatasource')); |   beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { | ||||||
|   beforeEach(function() { |     ctx.$q = $q; | ||||||
|     ctx.ds = new ctx.service({jsonData: {}}); |     ctx.$httpBackend =  $httpBackend; | ||||||
|   }); |     ctx.$rootScope = $rootScope; | ||||||
|  |     ctx.$injector = $injector; | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   function createDatasource(instanceSettings) { | ||||||
|  |     instanceSettings.jsonData = instanceSettings.jsonData || {}; | ||||||
|  |     ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings}); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   describe('When testing datasource with index pattern', function() { |   describe('When testing datasource with index pattern', function() { | ||||||
|     beforeEach(function() { |     beforeEach(function() { | ||||||
|       ctx.ds = new ctx.service({ |       createDatasource({url: 'http://es.com', index: '[asd-]YYYY.MM.DD', jsonData: {interval: 'Daily'}}); | ||||||
|         url: 'http://es.com', |  | ||||||
|         index: '[asd-]YYYY.MM.DD', |  | ||||||
|         jsonData: { interval: 'Daily' } |  | ||||||
|       }); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should translate index pattern to current day', function() { |     it('should translate index pattern to current day', function() { | ||||||
|  | @ -44,11 +48,7 @@ describe('ElasticDatasource', function() { | ||||||
|     var requestOptions, parts, header; |     var requestOptions, parts, header; | ||||||
| 
 | 
 | ||||||
|     beforeEach(function() { |     beforeEach(function() { | ||||||
|       ctx.ds = new ctx.service({ |       createDatasource({url: 'http://es.com', index: '[asd-]YYYY.MM.DD', jsonData: {interval: 'Daily'}}); | ||||||
|         url: 'http://es.com', |  | ||||||
|         index: '[asd-]YYYY.MM.DD', |  | ||||||
|         jsonData: { interval: 'Daily' } |  | ||||||
|       }); |  | ||||||
| 
 | 
 | ||||||
|       ctx.backendSrv.datasourceRequest = function(options) { |       ctx.backendSrv.datasourceRequest = function(options) { | ||||||
|         requestOptions = options; |         requestOptions = options; | ||||||
|  | @ -83,7 +83,7 @@ describe('ElasticDatasource', function() { | ||||||
|     var requestOptions, parts, header; |     var requestOptions, parts, header; | ||||||
| 
 | 
 | ||||||
|     beforeEach(function() { |     beforeEach(function() { | ||||||
|       ctx.ds = new ctx.service({url: 'http://es.com', index: 'test', jsonData: {}}); |       createDatasource({url: 'http://es.com', index: 'test'}); | ||||||
| 
 | 
 | ||||||
|       ctx.backendSrv.datasourceRequest = function(options) { |       ctx.backendSrv.datasourceRequest = function(options) { | ||||||
|         requestOptions = options; |         requestOptions = options; | ||||||
|  |  | ||||||
|  | @ -1,30 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular' |  | ||||||
| ], |  | ||||||
| function (angular) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.services'); |  | ||||||
| 
 |  | ||||||
|   module.factory('GrafanaDatasource', function($q, backendSrv) { |  | ||||||
| 
 |  | ||||||
|     function GrafanaDatasource() { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GrafanaDatasource.prototype.query = function(options) { |  | ||||||
|       return backendSrv.get('/api/metrics/test', { |  | ||||||
|         from: options.range.from.valueOf(), |  | ||||||
|         to: options.range.to.valueOf(), |  | ||||||
|         maxDataPoints: options.maxDataPoints |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     GrafanaDatasource.prototype.metricFindQuery = function() { |  | ||||||
|       return $q.when([]); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return GrafanaDatasource; |  | ||||||
| 
 |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
| }); |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
| ], |  | ||||||
| function (angular) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.directives'); |  | ||||||
| 
 |  | ||||||
|   module.directive('metricQueryEditorGrafana', function() { |  | ||||||
|     return {templateUrl: 'app/plugins/datasource/grafana/partials/query.editor.html'}; |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
| }); |  | ||||||
|  | @ -1,11 +1,8 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "Grafana", |   "name": "Grafana", | ||||||
|  |   "id": "grafana", | ||||||
|  | 
 | ||||||
|   "builtIn": true, |   "builtIn": true, | ||||||
| 
 |  | ||||||
|   "type": "grafana", |  | ||||||
|   "serviceName": "GrafanaDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/grafana/datasource", |  | ||||||
|   "metrics": true |   "metrics": true | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | declare var Datasource: any; | ||||||
|  | export default Datasource; | ||||||
|  | 
 | ||||||
|  | @ -4,7 +4,6 @@ define([ | ||||||
|   'jquery', |   'jquery', | ||||||
|   'app/core/config', |   'app/core/config', | ||||||
|   'app/core/utils/datemath', |   'app/core/utils/datemath', | ||||||
|   './directives', |  | ||||||
|   './query_ctrl', |   './query_ctrl', | ||||||
|   './func_editor', |   './func_editor', | ||||||
|   './add_graphite_func', |   './add_graphite_func', | ||||||
|  | @ -12,20 +11,16 @@ define([ | ||||||
| function (angular, _, $, config, dateMath) { | function (angular, _, $, config, dateMath) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.services'); |   /** @ngInject */ | ||||||
|  |   function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) { | ||||||
|  |     this.basicAuth = instanceSettings.basicAuth; | ||||||
|  |     this.url = instanceSettings.url; | ||||||
|  |     this.name = instanceSettings.name; | ||||||
|  |     this.cacheTimeout = instanceSettings.cacheTimeout; | ||||||
|  |     this.withCredentials = instanceSettings.withCredentials; | ||||||
|  |     this.render_method = instanceSettings.render_method || 'POST'; | ||||||
| 
 | 
 | ||||||
|   module.factory('GraphiteDatasource', function($q, backendSrv, templateSrv) { |     this.query = function(options) { | ||||||
| 
 |  | ||||||
|     function GraphiteDatasource(datasource) { |  | ||||||
|       this.basicAuth = datasource.basicAuth; |  | ||||||
|       this.url = datasource.url; |  | ||||||
|       this.name = datasource.name; |  | ||||||
|       this.cacheTimeout = datasource.cacheTimeout; |  | ||||||
|       this.withCredentials = datasource.withCredentials; |  | ||||||
|       this.render_method = datasource.render_method || 'POST'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GraphiteDatasource.prototype.query = function(options) { |  | ||||||
|       try { |       try { | ||||||
|         var graphOptions = { |         var graphOptions = { | ||||||
|           from: this.translateTime(options.rangeRaw.from, false), |           from: this.translateTime(options.rangeRaw.from, false), | ||||||
|  | @ -62,7 +57,7 @@ function (angular, _, $, config, dateMath) { | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.convertDataPointsToMs = function(result) { |     this.convertDataPointsToMs = function(result) { | ||||||
|       if (!result || !result.data) { return []; } |       if (!result || !result.data) { return []; } | ||||||
|       for (var i = 0; i < result.data.length; i++) { |       for (var i = 0; i < result.data.length; i++) { | ||||||
|         var series = result.data[i]; |         var series = result.data[i]; | ||||||
|  | @ -73,7 +68,7 @@ function (angular, _, $, config, dateMath) { | ||||||
|       return result; |       return result; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.annotationQuery = function(options) { |     this.annotationQuery = function(options) { | ||||||
|       // Graphite metric as annotation
 |       // Graphite metric as annotation
 | ||||||
|       if (options.annotation.target) { |       if (options.annotation.target) { | ||||||
|         var target = templateSrv.replace(options.annotation.target); |         var target = templateSrv.replace(options.annotation.target); | ||||||
|  | @ -85,50 +80,49 @@ function (angular, _, $, config, dateMath) { | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         return this.query(graphiteQuery) |         return this.query(graphiteQuery) | ||||||
|           .then(function(result) { |         .then(function(result) { | ||||||
|             var list = []; |           var list = []; | ||||||
| 
 | 
 | ||||||
|             for (var i = 0; i < result.data.length; i++) { |           for (var i = 0; i < result.data.length; i++) { | ||||||
|               var target = result.data[i]; |             var target = result.data[i]; | ||||||
| 
 | 
 | ||||||
|               for (var y = 0; y < target.datapoints.length; y++) { |             for (var y = 0; y < target.datapoints.length; y++) { | ||||||
|                 var datapoint = target.datapoints[y]; |               var datapoint = target.datapoints[y]; | ||||||
|                 if (!datapoint[0]) { continue; } |               if (!datapoint[0]) { continue; } | ||||||
| 
 | 
 | ||||||
|                 list.push({ |               list.push({ | ||||||
|                   annotation: options.annotation, |                 annotation: options.annotation, | ||||||
|                   time: datapoint[1], |                 time: datapoint[1], | ||||||
|                   title: target.target |                 title: target.target | ||||||
|                 }); |               }); | ||||||
|               } |  | ||||||
|             } |             } | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|             return list; |           return list; | ||||||
|           }); |         }); | ||||||
|       } |       } | ||||||
|       // Graphite event as annotation
 |       // Graphite event as annotation
 | ||||||
|       else { |       else { | ||||||
|         var tags = templateSrv.replace(options.annotation.tags); |         var tags = templateSrv.replace(options.annotation.tags); | ||||||
|         return this.events({range: options.rangeRaw, tags: tags}) |         return this.events({range: options.rangeRaw, tags: tags}).then(function(results) { | ||||||
|           .then(function(results) { |           var list = []; | ||||||
|             var list = []; |           for (var i = 0; i < results.data.length; i++) { | ||||||
|             for (var i = 0; i < results.data.length; i++) { |             var e = results.data[i]; | ||||||
|               var e = results.data[i]; |  | ||||||
| 
 | 
 | ||||||
|               list.push({ |             list.push({ | ||||||
|                 annotation: options.annotation, |               annotation: options.annotation, | ||||||
|                 time: e.when * 1000, |               time: e.when * 1000, | ||||||
|                 title: e.what, |               title: e.what, | ||||||
|                 tags: e.tags, |               tags: e.tags, | ||||||
|                 text: e.data |               text: e.data | ||||||
|               }); |             }); | ||||||
|             } |           } | ||||||
|             return list; |           return list; | ||||||
|           }); |         }); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.events = function(options) { |     this.events = function(options) { | ||||||
|       try { |       try { | ||||||
|         var tags = ''; |         var tags = ''; | ||||||
|         if (options.tags) { |         if (options.tags) { | ||||||
|  | @ -146,7 +140,7 @@ function (angular, _, $, config, dateMath) { | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.translateTime = function(date, roundUp) { |     this.translateTime = function(date, roundUp) { | ||||||
|       if (_.isString(date)) { |       if (_.isString(date)) { | ||||||
|         if (date === 'now') { |         if (date === 'now') { | ||||||
|           return 'now'; |           return 'now'; | ||||||
|  | @ -178,7 +172,7 @@ function (angular, _, $, config, dateMath) { | ||||||
|       return date.unix(); |       return date.unix(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.metricFindQuery = function(query) { |     this.metricFindQuery = function(query) { | ||||||
|       var interpolated; |       var interpolated; | ||||||
|       try { |       try { | ||||||
|         interpolated = encodeURIComponent(templateSrv.replace(query)); |         interpolated = encodeURIComponent(templateSrv.replace(query)); | ||||||
|  | @ -198,24 +192,24 @@ function (angular, _, $, config, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.testDatasource = function() { |     this.testDatasource = function() { | ||||||
|       return this.metricFindQuery('*').then(function () { |       return this.metricFindQuery('*').then(function () { | ||||||
|         return { status: "success", message: "Data source is working", title: "Success" }; |         return { status: "success", message: "Data source is working", title: "Success" }; | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.listDashboards = function(query) { |     this.listDashboards = function(query) { | ||||||
|       return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} }) |       return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} }) | ||||||
|       .then(function(results) { |       .then(function(results) { | ||||||
|         return results.data.dashboards; |         return results.data.dashboards; | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.loadDashboard = function(dashName) { |     this.loadDashboard = function(dashName) { | ||||||
|       return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) }); |       return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.doGraphiteRequest = function(options) { |     this.doGraphiteRequest = function(options) { | ||||||
|       if (this.basicAuth || this.withCredentials) { |       if (this.basicAuth || this.withCredentials) { | ||||||
|         options.withCredentials = true; |         options.withCredentials = true; | ||||||
|       } |       } | ||||||
|  | @ -230,9 +224,9 @@ function (angular, _, $, config, dateMath) { | ||||||
|       return backendSrv.datasourceRequest(options); |       return backendSrv.datasourceRequest(options); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; |     this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; | ||||||
| 
 | 
 | ||||||
|     GraphiteDatasource.prototype.buildGraphiteParams = function(options, scopedVars) { |     this.buildGraphiteParams = function(options, scopedVars) { | ||||||
|       var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; |       var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; | ||||||
|       var clean_options = [], targets = {}; |       var clean_options = [], targets = {}; | ||||||
|       var target, targetValue, i; |       var target, targetValue, i; | ||||||
|  | @ -296,9 +290,7 @@ function (angular, _, $, config, dateMath) { | ||||||
| 
 | 
 | ||||||
|       return clean_options; |       return clean_options; | ||||||
|     }; |     }; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     return GraphiteDatasource; |   return GraphiteDatasource; | ||||||
| 
 |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| define([ | define([ | ||||||
|   'angular', |   'angular', | ||||||
|  |   './datasource', | ||||||
| ], | ], | ||||||
| function (angular) { | function (angular, GraphiteDatasource) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.directives'); |   var module = angular.module('grafana.directives'); | ||||||
|  | @ -18,4 +19,11 @@ function (angular) { | ||||||
|     return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'}; |     return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'}; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsViewGraphite', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     Datasource: GraphiteDatasource, | ||||||
|  |   }; | ||||||
| }); | }); | ||||||
|  | @ -1,3 +1,2 @@ | ||||||
| <div ng-include="httpConfigPartialSrc"></div> | <datasource-http-settings></datasource-http-settings> | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,7 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |  | ||||||
|   "name": "Graphite", |   "name": "Graphite", | ||||||
| 
 |   "type": "datasource", | ||||||
|   "type": "graphite", |   "id": "graphite", | ||||||
|   "serviceName": "GraphiteDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/graphite/datasource", |  | ||||||
| 
 |  | ||||||
|   "partials": { |  | ||||||
|     "config": "app/plugins/datasource/graphite/partials/config.html" |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   "defaultMatchFormat": "glob", |   "defaultMatchFormat": "glob", | ||||||
|   "metrics": true, |   "metrics": true, | ||||||
|  |  | ||||||
|  | @ -1,19 +1,24 @@ | ||||||
| 
 | 
 | ||||||
| import "../datasource"; |  | ||||||
| import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | ||||||
| import helpers from 'test/specs/helpers'; | import helpers from 'test/specs/helpers'; | ||||||
|  | import Datasource from "../datasource"; | ||||||
| 
 | 
 | ||||||
| describe('graphiteDatasource', function() { | describe('graphiteDatasource', function() { | ||||||
|   var ctx = new helpers.ServiceTestContext(); |   var ctx = new helpers.ServiceTestContext(); | ||||||
|  |   var instanceSettings: any = {url: ['']}; | ||||||
| 
 | 
 | ||||||
|   beforeEach(angularMocks.module('grafana.core')); |   beforeEach(angularMocks.module('grafana.core')); | ||||||
|   beforeEach(angularMocks.module('grafana.services')); |   beforeEach(angularMocks.module('grafana.services')); | ||||||
| 
 |  | ||||||
|   beforeEach(ctx.providePhase(['backendSrv'])); |   beforeEach(ctx.providePhase(['backendSrv'])); | ||||||
|   beforeEach(ctx.createService('GraphiteDatasource')); |   beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { | ||||||
|  |     ctx.$q = $q; | ||||||
|  |     ctx.$httpBackend =  $httpBackend; | ||||||
|  |     ctx.$rootScope = $rootScope; | ||||||
|  |     ctx.$injector = $injector; | ||||||
|  |   })); | ||||||
| 
 | 
 | ||||||
|   beforeEach(function() { |   beforeEach(function() { | ||||||
|     ctx.ds = new ctx.service({ url: [''] }); |     ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings}); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('When querying influxdb with one target using query editor target spec', function() { |   describe('When querying influxdb with one target using query editor target spec', function() { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ define([ | ||||||
|   'app/core/utils/datemath', |   'app/core/utils/datemath', | ||||||
|   './influx_series', |   './influx_series', | ||||||
|   './influx_query', |   './influx_query', | ||||||
|   './directives', |  | ||||||
|   './query_ctrl', |   './query_ctrl', | ||||||
| ], | ], | ||||||
| function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | ||||||
|  | @ -12,27 +11,22 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | ||||||
| 
 | 
 | ||||||
|   InfluxQuery = InfluxQuery.default; |   InfluxQuery = InfluxQuery.default; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.services'); |   function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) { | ||||||
|  |     this.type = 'influxdb'; | ||||||
|  |     this.urls = _.map(instanceSettings.url.split(','), function(url) { | ||||||
|  |       return url.trim(); | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|   module.factory('InfluxDatasource', function($q, backendSrv, templateSrv) { |     this.username = instanceSettings.username; | ||||||
|  |     this.password = instanceSettings.password; | ||||||
|  |     this.name = instanceSettings.name; | ||||||
|  |     this.database = instanceSettings.database; | ||||||
|  |     this.basicAuth = instanceSettings.basicAuth; | ||||||
| 
 | 
 | ||||||
|     function InfluxDatasource(datasource) { |     this.supportAnnotations = true; | ||||||
|       this.type = 'influxdb'; |     this.supportMetrics = true; | ||||||
|       this.urls = _.map(datasource.url.split(','), function(url) { |  | ||||||
|         return url.trim(); |  | ||||||
|       }); |  | ||||||
| 
 | 
 | ||||||
|       this.username = datasource.username; |     this.query = function(options) { | ||||||
|       this.password = datasource.password; |  | ||||||
|       this.name = datasource.name; |  | ||||||
|       this.database = datasource.database; |  | ||||||
|       this.basicAuth = datasource.basicAuth; |  | ||||||
| 
 |  | ||||||
|       this.supportAnnotations = true; |  | ||||||
|       this.supportMetrics = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     InfluxDatasource.prototype.query = function(options) { |  | ||||||
|       var timeFilter = getTimeFilter(options); |       var timeFilter = getTimeFilter(options); | ||||||
|       var queryTargets = []; |       var queryTargets = []; | ||||||
|       var i, y; |       var i, y; | ||||||
|  | @ -93,7 +87,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     InfluxDatasource.prototype.annotationQuery = function(options) { |     this.annotationQuery = function(options) { | ||||||
|       var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw}); |       var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw}); | ||||||
|       var query = options.annotation.query.replace('$timeFilter', timeFilter); |       var query = options.annotation.query.replace('$timeFilter', timeFilter); | ||||||
|       query = templateSrv.replace(query); |       query = templateSrv.replace(query); | ||||||
|  | @ -106,7 +100,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     InfluxDatasource.prototype.metricFindQuery = function (query) { |     this.metricFindQuery = function (query) { | ||||||
|       var interpolated; |       var interpolated; | ||||||
|       try { |       try { | ||||||
|         interpolated = templateSrv.replace(query); |         interpolated = templateSrv.replace(query); | ||||||
|  | @ -133,17 +127,17 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     InfluxDatasource.prototype._seriesQuery = function(query) { |     this._seriesQuery = function(query) { | ||||||
|       return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); |       return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     InfluxDatasource.prototype.testDatasource = function() { |     this.testDatasource = function() { | ||||||
|       return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { |       return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { | ||||||
|         return { status: "success", message: "Data source is working", title: "Success" }; |         return { status: "success", message: "Data source is working", title: "Success" }; | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     InfluxDatasource.prototype._influxRequest = function(method, url, data) { |     this._influxRequest = function(method, url, data) { | ||||||
|       var self = this; |       var self = this; | ||||||
| 
 | 
 | ||||||
|       var currentUrl = self.urls.shift(); |       var currentUrl = self.urls.shift(); | ||||||
|  | @ -219,9 +213,8 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { | ||||||
|       } |       } | ||||||
|       return (date.valueOf() / 1000).toFixed(0) + 's'; |       return (date.valueOf() / 1000).toFixed(0) + 's'; | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     return InfluxDatasource; |   return InfluxDatasource; | ||||||
| 
 |  | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| define([ | define([ | ||||||
|   'angular', |   'angular', | ||||||
|  |   './datasource', | ||||||
| ], | ], | ||||||
| function (angular) { | function (angular, InfluxDatasource) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.directives'); |   var module = angular.module('grafana.directives'); | ||||||
|  | @ -18,4 +19,11 @@ function (angular) { | ||||||
|     return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'}; |     return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'}; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsViewInfluxdb', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/influxdb/partials/config.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     Datasource: InfluxDatasource | ||||||
|  |   }; | ||||||
| }); | }); | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| <div ng-include="httpConfigPartialSrc"></div> | <datasource-http-settings></datasource-http-settings> | ||||||
| <br> |  | ||||||
| 
 | 
 | ||||||
| <h5>InfluxDB Details</h5> | <h5>InfluxDB Details</h5> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,7 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "InfluxDB 0.9.x", |   "name": "InfluxDB 0.9.x", | ||||||
| 
 |   "id": "influxdb", | ||||||
|   "type": "influxdb", |  | ||||||
|   "serviceName": "InfluxDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/influxdb/datasource", |  | ||||||
| 
 |  | ||||||
|   "partials": { |  | ||||||
|     "config": "app/plugins/datasource/influxdb/partials/config.html" |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   "defaultMatchFormat": "regex values", |   "defaultMatchFormat": "regex values", | ||||||
|   "metrics": true, |   "metrics": true, | ||||||
|  |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
|   'lodash', |  | ||||||
| ], |  | ||||||
| function (angular, _) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.services'); |  | ||||||
| 
 |  | ||||||
|   module.factory('MixedDatasource', function($q, backendSrv, datasourceSrv) { |  | ||||||
| 
 |  | ||||||
|     function MixedDatasource() { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     MixedDatasource.prototype.query = function(options) { |  | ||||||
|       var sets = _.groupBy(options.targets, 'datasource'); |  | ||||||
|       var promises = _.map(sets, function(targets) { |  | ||||||
|         return datasourceSrv.get(targets[0].datasource).then(function(ds) { |  | ||||||
|           var opt = angular.copy(options); |  | ||||||
|           opt.targets = targets; |  | ||||||
|           return ds.query(opt); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       return $q.all(promises).then(function(results) { |  | ||||||
|         return { data: _.flatten(_.pluck(results, 'data')) }; |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return MixedDatasource; |  | ||||||
| 
 |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
| }); |  | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | ///<reference path="../../../headers/common.d.ts" />
 | ||||||
|  | 
 | ||||||
|  | import angular from 'angular'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | 
 | ||||||
|  | class MixedDatasource { | ||||||
|  | 
 | ||||||
|  |   constructor(private $q, private datasourceSrv) { | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   query(options) { | ||||||
|  |     var sets = _.groupBy(options.targets, 'datasource'); | ||||||
|  |     var promises = _.map(sets, targets => { | ||||||
|  |       var dsName = targets[0].datasource; | ||||||
|  |       if (dsName === '-- Mixed --') { | ||||||
|  |         return this.$q([]); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return this.datasourceSrv.get(dsName).then(function(ds) { | ||||||
|  |         var opt = angular.copy(options); | ||||||
|  |         opt.targets = targets; | ||||||
|  |         return ds.query(opt); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return this.$q.all(promises).then(function(results) { | ||||||
|  |       return { data: _.flatten(_.pluck(results, 'data')) }; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export {MixedDatasource, MixedDatasource as Datasource} | ||||||
|  | @ -1,12 +1,9 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "Mixed datasource", |   "name": "Mixed datasource", | ||||||
|  |   "id": "mixed", | ||||||
|  | 
 | ||||||
|   "builtIn": true, |   "builtIn": true, | ||||||
|   "mixed": true, |   "mixed": true, | ||||||
| 
 |  | ||||||
|   "type": "mixed", |  | ||||||
|   "serviceName": "MixedDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/mixed/datasource", |  | ||||||
|   "metrics": true |   "metrics": true | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | declare var Datasource: any; | ||||||
|  | export default Datasource; | ||||||
|  | 
 | ||||||
|  | @ -3,25 +3,19 @@ define([ | ||||||
|   'lodash', |   'lodash', | ||||||
|   'app/core/utils/datemath', |   'app/core/utils/datemath', | ||||||
|   'moment', |   'moment', | ||||||
|   './directives', |  | ||||||
|   './queryCtrl', |   './queryCtrl', | ||||||
| ], | ], | ||||||
| function (angular, _, dateMath) { | function (angular, _, dateMath) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.services'); |   function OpenTSDBDatasource(instanceSettings, $q, backendSrv, templateSrv) { | ||||||
| 
 |     this.type = 'opentsdb'; | ||||||
|   module.factory('OpenTSDBDatasource', function($q, backendSrv, templateSrv) { |     this.url = instanceSettings.url; | ||||||
| 
 |     this.name = instanceSettings.name; | ||||||
|     function OpenTSDBDatasource(datasource) { |     this.supportMetrics = true; | ||||||
|       this.type = 'opentsdb'; |  | ||||||
|       this.url = datasource.url; |  | ||||||
|       this.name = datasource.name; |  | ||||||
|       this.supportMetrics = true; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // Called once per panel (graph)
 |     // Called once per panel (graph)
 | ||||||
|     OpenTSDBDatasource.prototype.query = function(options) { |     this.query = function(options) { | ||||||
|       var start = convertToTSDBTime(options.rangeRaw.from, false); |       var start = convertToTSDBTime(options.rangeRaw.from, false); | ||||||
|       var end = convertToTSDBTime(options.rangeRaw.to, true); |       var end = convertToTSDBTime(options.rangeRaw.to, true); | ||||||
|       var qs = []; |       var qs = []; | ||||||
|  | @ -60,7 +54,7 @@ function (angular, _, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) { |     this.performTimeSeriesQuery = function(queries, start, end) { | ||||||
|       var reqBody = { |       var reqBody = { | ||||||
|         start: start, |         start: start, | ||||||
|         queries: queries |         queries: queries | ||||||
|  | @ -80,13 +74,13 @@ function (angular, _, dateMath) { | ||||||
|       return backendSrv.datasourceRequest(options); |       return backendSrv.datasourceRequest(options); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype._performSuggestQuery = function(query, type) { |     this._performSuggestQuery = function(query, type) { | ||||||
|       return this._get('/api/suggest', {type: type, q: query, max: 1000}).then(function(result) { |       return this._get('/api/suggest', {type: type, q: query, max: 1000}).then(function(result) { | ||||||
|         return result.data; |         return result.data; | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype._performMetricKeyValueLookup = function(metric, key) { |     this._performMetricKeyValueLookup = function(metric, key) { | ||||||
|       if(!metric || !key) { |       if(!metric || !key) { | ||||||
|         return $q.when([]); |         return $q.when([]); | ||||||
|       } |       } | ||||||
|  | @ -105,7 +99,7 @@ function (angular, _, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype._performMetricKeyLookup = function(metric) { |     this._performMetricKeyLookup = function(metric) { | ||||||
|       if(!metric) { return $q.when([]); } |       if(!metric) { return $q.when([]); } | ||||||
| 
 | 
 | ||||||
|       return this._get('/api/search/lookup', {m: metric, limit: 1000}).then(function(result) { |       return this._get('/api/search/lookup', {m: metric, limit: 1000}).then(function(result) { | ||||||
|  | @ -122,7 +116,7 @@ function (angular, _, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype._get = function(relativeUrl, params) { |     this._get = function(relativeUrl, params) { | ||||||
|       return backendSrv.datasourceRequest({ |       return backendSrv.datasourceRequest({ | ||||||
|         method: 'GET', |         method: 'GET', | ||||||
|         url: this.url + relativeUrl, |         url: this.url + relativeUrl, | ||||||
|  | @ -130,7 +124,7 @@ function (angular, _, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype.metricFindQuery = function(query) { |     this.metricFindQuery = function(query) { | ||||||
|       if (!query) { return $q.when([]); } |       if (!query) { return $q.when([]); } | ||||||
| 
 | 
 | ||||||
|       var interpolated; |       var interpolated; | ||||||
|  | @ -181,14 +175,14 @@ function (angular, _, dateMath) { | ||||||
|       return $q.when([]); |       return $q.when([]); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     OpenTSDBDatasource.prototype.testDatasource = function() { |     this.testDatasource = function() { | ||||||
|       return this._performSuggestQuery('cpu', 'metrics').then(function () { |       return this._performSuggestQuery('cpu', 'metrics').then(function () { | ||||||
|         return { status: "success", message: "Data source is working", title: "Success" }; |         return { status: "success", message: "Data source is working", title: "Success" }; | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     var aggregatorsPromise = null; |     var aggregatorsPromise = null; | ||||||
|     OpenTSDBDatasource.prototype.getAggregators = function() { |     this.getAggregators = function() { | ||||||
|       if (aggregatorsPromise) { return aggregatorsPromise; } |       if (aggregatorsPromise) { return aggregatorsPromise; } | ||||||
| 
 | 
 | ||||||
|       aggregatorsPromise =  this._get('/api/aggregators').then(function(result) { |       aggregatorsPromise =  this._get('/api/aggregators').then(function(result) { | ||||||
|  | @ -311,7 +305,7 @@ function (angular, _, dateMath) { | ||||||
|       return date.valueOf(); |       return date.valueOf(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return OpenTSDBDatasource; |   } | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|  |   return OpenTSDBDatasource; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| define([ | define([ | ||||||
|   'angular', |   'angular', | ||||||
|  |   './datasource', | ||||||
| ], | ], | ||||||
| function (angular) { | function (angular, OpenTsDatasource) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.directives'); |   var module = angular.module('grafana.directives'); | ||||||
|  | @ -13,4 +14,11 @@ function (angular) { | ||||||
|     }; |     }; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsViewOpentsdb', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/opentsdb/partials/config.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     Datasource: OpenTsDatasource | ||||||
|  |   }; | ||||||
| }); | }); | ||||||
|  | @ -1,4 +1,2 @@ | ||||||
| <div ng-include="httpConfigPartialSrc"></div> | <datasource-http-settings></datasource-http-settings> | ||||||
| 
 |  | ||||||
| <br> |  | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,7 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "OpenTSDB", |   "name": "OpenTSDB", | ||||||
| 
 |   "id": "opentsdb", | ||||||
|   "type": "opentsdb", |  | ||||||
|   "serviceName": "OpenTSDBDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/opentsdb/datasource", |  | ||||||
| 
 |  | ||||||
|   "partials": { |  | ||||||
|     "config": "app/plugins/datasource/opentsdb/partials/config.html" |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   "metrics": true, |   "metrics": true, | ||||||
|   "defaultMatchFormat": "pipe" |   "defaultMatchFormat": "pipe" | ||||||
|  |  | ||||||
|  | @ -0,0 +1,71 @@ | ||||||
|  | import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | ||||||
|  | import helpers from 'test/specs/helpers'; | ||||||
|  | import Datasource from "../datasource"; | ||||||
|  | 
 | ||||||
|  | describe('opentsdb', function() { | ||||||
|  |   var ctx = new helpers.ServiceTestContext(); | ||||||
|  |   var instanceSettings = {url: '' }; | ||||||
|  | 
 | ||||||
|  |   beforeEach(angularMocks.module('grafana.core')); | ||||||
|  |   beforeEach(angularMocks.module('grafana.services')); | ||||||
|  |   beforeEach(ctx.providePhase(['backendSrv'])); | ||||||
|  | 
 | ||||||
|  |   beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { | ||||||
|  |     ctx.$q = $q; | ||||||
|  |     ctx.$httpBackend =  $httpBackend; | ||||||
|  |     ctx.$rootScope = $rootScope; | ||||||
|  |     ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings}); | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   describe('When performing metricFindQuery', function() { | ||||||
|  |     var results; | ||||||
|  |     var requestOptions; | ||||||
|  | 
 | ||||||
|  |     beforeEach(function() { | ||||||
|  |       ctx.backendSrv.datasourceRequest = function(options) { | ||||||
|  |         requestOptions = options; | ||||||
|  |         return ctx.$q.when({data: [{ target: 'prod1.count', datapoints: [[10, 1], [12,1]] }]}); | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('metrics() should generate api suggest query', function() { | ||||||
|  |       ctx.ds.metricFindQuery('metrics(pew)').then(function(data) { results = data; }); | ||||||
|  |       ctx.$rootScope.$apply(); | ||||||
|  |       expect(requestOptions.url).to.be('/api/suggest'); | ||||||
|  |       expect(requestOptions.params.type).to.be('metrics'); | ||||||
|  |       expect(requestOptions.params.q).to.be('pew'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('tag_names(cpu) should generate looku  query', function() { | ||||||
|  |       ctx.ds.metricFindQuery('tag_names(cpu)').then(function(data) { results = data; }); | ||||||
|  |       ctx.$rootScope.$apply(); | ||||||
|  |       expect(requestOptions.url).to.be('/api/search/lookup'); | ||||||
|  |       expect(requestOptions.params.m).to.be('cpu'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('tag_values(cpu, test) should generate looku  query', function() { | ||||||
|  |       ctx.ds.metricFindQuery('tag_values(cpu, hostname)').then(function(data) { results = data; }); | ||||||
|  |       ctx.$rootScope.$apply(); | ||||||
|  |       expect(requestOptions.url).to.be('/api/search/lookup'); | ||||||
|  |       expect(requestOptions.params.m).to.be('cpu{hostname=*}'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('suggest_tagk() should generate api suggest query', function() { | ||||||
|  |       ctx.ds.metricFindQuery('suggest_tagk(foo)').then(function(data) { results = data; }); | ||||||
|  |       ctx.$rootScope.$apply(); | ||||||
|  |       expect(requestOptions.url).to.be('/api/suggest'); | ||||||
|  |       expect(requestOptions.params.type).to.be('tagk'); | ||||||
|  |       expect(requestOptions.params.q).to.be('foo'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('suggest_tagv() should generate api suggest query', function() { | ||||||
|  |       ctx.ds.metricFindQuery('suggest_tagv(bar)').then(function(data) { results = data; }); | ||||||
|  |       ctx.$rootScope.$apply(); | ||||||
|  |       expect(requestOptions.url).to.be('/api/suggest'); | ||||||
|  |       expect(requestOptions.params.type).to.be('tagv'); | ||||||
|  |       expect(requestOptions.params.q).to.be('bar'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | declare var Datasource: any; | ||||||
|  | export default Datasource; | ||||||
|  | 
 | ||||||
|  | @ -3,31 +3,25 @@ define([ | ||||||
|   'lodash', |   'lodash', | ||||||
|   'moment', |   'moment', | ||||||
|   'app/core/utils/datemath', |   'app/core/utils/datemath', | ||||||
|   './directives', |  | ||||||
|   './query_ctrl', |   './query_ctrl', | ||||||
| ], | ], | ||||||
| function (angular, _, moment, dateMath) { | function (angular, _, moment, dateMath) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.services'); |  | ||||||
| 
 |  | ||||||
|   var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/; |   var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/; | ||||||
| 
 | 
 | ||||||
|   module.factory('PrometheusDatasource', function($q, backendSrv, templateSrv) { |   function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv) { | ||||||
|  |     this.type = 'prometheus'; | ||||||
|  |     this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; | ||||||
|  |     this.name = instanceSettings.name; | ||||||
|  |     this.supportMetrics = true; | ||||||
|  |     this.url = instanceSettings.url; | ||||||
|  |     this.directUrl = instanceSettings.directUrl; | ||||||
|  |     this.basicAuth = instanceSettings.basicAuth; | ||||||
|  |     this.withCredentials = instanceSettings.withCredentials; | ||||||
|  |     this.lastErrors = {}; | ||||||
| 
 | 
 | ||||||
|     function PrometheusDatasource(datasource) { |     this._request = function(method, url) { | ||||||
|       this.type = 'prometheus'; |  | ||||||
|       this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; |  | ||||||
|       this.name = datasource.name; |  | ||||||
|       this.supportMetrics = true; |  | ||||||
|       this.url = datasource.url; |  | ||||||
|       this.directUrl = datasource.directUrl; |  | ||||||
|       this.basicAuth = datasource.basicAuth; |  | ||||||
|       this.withCredentials = datasource.withCredentials; |  | ||||||
|       this.lastErrors = {}; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     PrometheusDatasource.prototype._request = function(method, url) { |  | ||||||
|       var options = { |       var options = { | ||||||
|         url: this.url + url, |         url: this.url + url, | ||||||
|         method: method |         method: method | ||||||
|  | @ -46,7 +40,7 @@ function (angular, _, moment, dateMath) { | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Called once per panel (graph)
 |     // Called once per panel (graph)
 | ||||||
|     PrometheusDatasource.prototype.query = function(options) { |     this.query = function(options) { | ||||||
|       var start = getPrometheusTime(options.range.from, false); |       var start = getPrometheusTime(options.range.from, false); | ||||||
|       var end = getPrometheusTime(options.range.to, true); |       var end = getPrometheusTime(options.range.to, true); | ||||||
| 
 | 
 | ||||||
|  | @ -86,31 +80,31 @@ function (angular, _, moment, dateMath) { | ||||||
| 
 | 
 | ||||||
|       var self = this; |       var self = this; | ||||||
|       return $q.all(allQueryPromise) |       return $q.all(allQueryPromise) | ||||||
|         .then(function(allResponse) { |       .then(function(allResponse) { | ||||||
|           var result = []; |         var result = []; | ||||||
| 
 | 
 | ||||||
|           _.each(allResponse, function(response, index) { |         _.each(allResponse, function(response, index) { | ||||||
|             if (response.status === 'error') { |           if (response.status === 'error') { | ||||||
|               self.lastErrors.query = response.error; |             self.lastErrors.query = response.error; | ||||||
|               throw response.error; |             throw response.error; | ||||||
|             } |           } | ||||||
|             delete self.lastErrors.query; |           delete self.lastErrors.query; | ||||||
| 
 | 
 | ||||||
|             _.each(response.data.data.result, function(metricData) { |           _.each(response.data.data.result, function(metricData) { | ||||||
|               result.push(transformMetricData(metricData, options.targets[index])); |             result.push(transformMetricData(metricData, options.targets[index])); | ||||||
|             }); |  | ||||||
|           }); |           }); | ||||||
| 
 |  | ||||||
|           return { data: result }; |  | ||||||
|         }); |         }); | ||||||
|  | 
 | ||||||
|  |         return { data: result }; | ||||||
|  |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     PrometheusDatasource.prototype.performTimeSeriesQuery = function(query, start, end) { |     this.performTimeSeriesQuery = function(query, start, end) { | ||||||
|       var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step; |       var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step; | ||||||
|       return this._request('GET', url); |       return this._request('GET', url); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     PrometheusDatasource.prototype.performSuggestQuery = function(query) { |     this.performSuggestQuery = function(query) { | ||||||
|       var url = '/api/v1/label/__name__/values'; |       var url = '/api/v1/label/__name__/values'; | ||||||
| 
 | 
 | ||||||
|       return this._request('GET', url).then(function(result) { |       return this._request('GET', url).then(function(result) { | ||||||
|  | @ -120,7 +114,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     PrometheusDatasource.prototype.metricFindQuery = function(query) { |     this.metricFindQuery = function(query) { | ||||||
|       if (!query) { return $q.when([]); } |       if (!query) { return $q.when([]); } | ||||||
| 
 | 
 | ||||||
|       var interpolated; |       var interpolated; | ||||||
|  | @ -196,7 +190,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     PrometheusDatasource.prototype.testDatasource = function() { |     this.testDatasource = function() { | ||||||
|       return this.metricFindQuery('metrics(.*)').then(function() { |       return this.metricFindQuery('metrics(.*)').then(function() { | ||||||
|         return { status: 'success', message: 'Data source is working', title: 'Success' }; |         return { status: 'success', message: 'Data source is working', title: 'Success' }; | ||||||
|       }); |       }); | ||||||
|  | @ -276,8 +270,7 @@ function (angular, _, moment, dateMath) { | ||||||
|       } |       } | ||||||
|       return (date.valueOf() / 1000).toFixed(0); |       return (date.valueOf() / 1000).toFixed(0); | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|     return PrometheusDatasource; |   return PrometheusDatasource; | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| define([ | define([ | ||||||
|   'angular', |   'angular', | ||||||
|  |   './datasource', | ||||||
| ], | ], | ||||||
| function (angular) { | function (angular, PromDatasource) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var module = angular.module('grafana.directives'); |   var module = angular.module('grafana.directives'); | ||||||
|  | @ -10,4 +11,11 @@ function (angular) { | ||||||
|     return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'}; |     return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'}; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   module.directive('datasourceCustomSettingsViewPrometheus', function() { | ||||||
|  |     return {templateUrl: 'app/plugins/datasource/prometheus/partials/config.html'}; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     Datasource: PromDatasource | ||||||
|  |   }; | ||||||
| }); | }); | ||||||
|  | @ -1,4 +1,2 @@ | ||||||
| <div ng-include="httpConfigPartialSrc"></div> | <datasource-http-settings></datasource-http-settings> | ||||||
| 
 |  | ||||||
| <br> |  | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,7 @@ | ||||||
| { | { | ||||||
|   "pluginType": "datasource", |   "type": "datasource", | ||||||
|   "name": "Prometheus", |   "name": "Prometheus", | ||||||
| 
 |   "id": "prometheus", | ||||||
|   "type": "prometheus", |  | ||||||
|   "serviceName": "PrometheusDatasource", |  | ||||||
| 
 |  | ||||||
|   "module": "app/plugins/datasource/prometheus/datasource", |  | ||||||
| 
 |  | ||||||
|   "partials": { |  | ||||||
|     "config": "app/plugins/datasource/prometheus/partials/config.html" |  | ||||||
|   }, |  | ||||||
| 
 | 
 | ||||||
|   "metrics": true |   "metrics": true | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,17 +1,20 @@ | ||||||
| import '../datasource'; |  | ||||||
| import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; | ||||||
| import moment from 'moment'; | import moment from 'moment'; | ||||||
| import helpers from 'test/specs/helpers'; | import helpers from 'test/specs/helpers'; | ||||||
|  | import Datasource from '../datasource'; | ||||||
| 
 | 
 | ||||||
| describe('PrometheusDatasource', function() { | describe('PrometheusDatasource', function() { | ||||||
| 
 |  | ||||||
|   var ctx = new helpers.ServiceTestContext(); |   var ctx = new helpers.ServiceTestContext(); | ||||||
|  |   var instanceSettings = {url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' }; | ||||||
|  | 
 | ||||||
|   beforeEach(angularMocks.module('grafana.core')); |   beforeEach(angularMocks.module('grafana.core')); | ||||||
|   beforeEach(angularMocks.module('grafana.services')); |   beforeEach(angularMocks.module('grafana.services')); | ||||||
|   beforeEach(ctx.createService('PrometheusDatasource')); |   beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { | ||||||
|   beforeEach(function() { |     ctx.$q = $q; | ||||||
|     ctx.ds = new ctx.service({ url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' }); |     ctx.$httpBackend =  $httpBackend; | ||||||
|   }); |     ctx.$rootScope = $rootScope; | ||||||
|  |     ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings}); | ||||||
|  |   })); | ||||||
| 
 | 
 | ||||||
|   describe('When querying prometheus with one target using query editor target spec', function() { |   describe('When querying prometheus with one target using query editor target spec', function() { | ||||||
|     var results; |     var results; | ||||||
|  |  | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| define([ |  | ||||||
|   'angular', |  | ||||||
| ], |  | ||||||
| function (angular) { |  | ||||||
|   'use strict'; |  | ||||||
| 
 |  | ||||||
|   var module = angular.module('grafana.services'); |  | ||||||
| 
 |  | ||||||
|   module.factory('SqlDatasource', function() { |  | ||||||
| 
 |  | ||||||
|     function SqlDatasource() { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return SqlDatasource; |  | ||||||
| 
 |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
| }); |  | ||||||
|  | @ -1,53 +0,0 @@ | ||||||
| <h2>SQL Options</h2> |  | ||||||
| 
 |  | ||||||
| <div class="tight-form"> |  | ||||||
| 	<ul class="tight-form-list"> |  | ||||||
| 		<li class="tight-form-item" style="width: 80px"> |  | ||||||
| 			DB Type |  | ||||||
| 		</li> |  | ||||||
| 		<li> |  | ||||||
| 			<select class="input-medium tight-form-input" ng-model="current.jsonData.dbType" ng-options="f for f in ['sqlite3','mysql','postgres']"></select> |  | ||||||
| 		</li> |  | ||||||
| 		<li class="tight-form-item" style="width: 80px"> |  | ||||||
| 			Host |  | ||||||
| 		</li> |  | ||||||
| 		<li> |  | ||||||
| 			<input type="text" class="tight-form-input input-medium" ng-model='current.jsonData.host' placeholder="localhost:3306"> |  | ||||||
| 		</li> |  | ||||||
| 		<li class="tight-form-item" ng-if="current.jsonData.dbType === 'postgres'"> |  | ||||||
| 			SSL  |  | ||||||
| 			<input class="cr1" id="jsonData.ssl" type="checkbox" ng-model="current.jsonData.ssl" ng-checked="current.jsonData.ssl"> |  | ||||||
| 			<label for="jsonData.ssl" class="cr1"></label> |  | ||||||
| 		</li> |  | ||||||
| 	</ul> |  | ||||||
| 	<div class="clearfix"></div> |  | ||||||
| </div> |  | ||||||
| <div class="tight-form"> |  | ||||||
| 	<ul class="tight-form-list"> |  | ||||||
| 		<li class="tight-form-item" style="width: 80px"> |  | ||||||
| 			Database |  | ||||||
| 		</li> |  | ||||||
| 		<li> |  | ||||||
| 			<input type="text" class="tight-form-input input-medium" ng-model='current.database' placeholder=""> |  | ||||||
| 		</li> |  | ||||||
| 	</ul> |  | ||||||
| 	<div class="clearfix"></div> |  | ||||||
| </div> |  | ||||||
| <div class="tight-form"> |  | ||||||
| 	<ul class="tight-form-list"> |  | ||||||
| 		<li class="tight-form-item" style="width: 80px"> |  | ||||||
| 			User |  | ||||||
| 		</li> |  | ||||||
| 		<li> |  | ||||||
| 			<input type="text" class="tight-form-input input-medium" ng-model='current.user' placeholder=""> |  | ||||||
| 		</li> |  | ||||||
| 		<li class="tight-form-item" style="width: 80px"> |  | ||||||
| 			Password |  | ||||||
| 		</li> |  | ||||||
| 		<li> |  | ||||||
| 			<input type="password" class="tight-form-input input-medium" ng-model='current.password' placeholder=""> |  | ||||||
| 		</li> |  | ||||||
| 	</ul> |  | ||||||
| 	<div class="clearfix"></div> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| 
 |  | ||||||
| <div class="fluid-row" style="margin-top: 20px"> |  | ||||||
| 	<div class="span2"></div> |  | ||||||
| 	<div class="grafana-info-box span8"> |  | ||||||
| 		<h5>Test graph</h5> |  | ||||||
| 
 |  | ||||||
| 		<p> |  | ||||||
| 		This is just a test data source that generates random walk series. If this is your only data source |  | ||||||
| 		open the left side menu and navigate to the data sources admin screen and add your data sources. You can change |  | ||||||
| 		data source using the button to the left of the <strong>Add query</strong> button. |  | ||||||
| 		</p> |  | ||||||
| 	</div> |  | ||||||
| 	<div class="span2"></div> |  | ||||||
| 
 |  | ||||||
| 	<div class="clearfix"></div> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue