Merge branch 'apps'

This commit is contained in:
Torkel Ödegaard 2016-01-12 15:41:15 +01:00
commit e5b3f27a30
160 changed files with 1755 additions and 1164 deletions

View File

@ -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)
} }

75
pkg/api/api_plugin.go Normal file
View File

@ -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}
}

59
pkg/api/app_settings.go Normal file
View File

@ -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")
}

View File

@ -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)
}
} }

33
pkg/api/dtos/apps.go Normal file
View File

@ -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
}

View File

@ -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"`
} }

View File

@ -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"`
}

View File

@ -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,
} }

View File

@ -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

View File

@ -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")

View File

@ -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)
}

View File

@ -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
} }

View File

@ -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

43
pkg/plugins/app_plugin.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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),
}
} }

View File

@ -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
}

View File

@ -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)
} }

View File

@ -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")
})
} }

75
pkg/plugins/queries.go Normal file
View File

@ -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
}

View File

@ -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
}
})
}

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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
}
})
}

View File

@ -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',

View File

@ -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)
}); });
}); });
}; };

View File

@ -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));
} }
}; };

View File

@ -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',

View File

@ -46,6 +46,10 @@ function (angular, _, coreModule) {
}, timeout); }, timeout);
} }
if (!$rootScope.$$phase) {
$rootScope.$digest();
}
return(newAlert); return(newAlert);
}; };

View File

@ -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;

View File

@ -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> &nbsp; <i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i> &nbsp;
{{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>

View File

@ -0,0 +1,2 @@
import './edit_ctrl';
import './list_ctrl';

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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}} &nbsp; &nbsp; 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>
&nbsp; &nbsp; &nbsp;
<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>

View File

@ -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>
&nbsp; &nbsp;
<span class="label label-info" ng-if="app.enabled">
Enabled
</span>
&nbsp;
<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>

View File

@ -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;

View File

@ -0,0 +1,4 @@
define([
'./list_ctrl',
'./edit_ctrl',
], function () {});

View File

@ -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();
}); });
}); });

View File

@ -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>

View File

@ -53,3 +53,5 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
<br>

View File

@ -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 () {});

View File

@ -1,3 +0,0 @@
<div>
{{current.type}} plugin does not have any additional config.
</div>

View File

@ -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&nbsp;
<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>

View File

@ -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> &nbsp;
{{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&nbsp;
<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>

View File

@ -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();
});
});

View File

@ -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();
};
}
};
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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',

View File

@ -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>

View File

@ -0,0 +1,3 @@
declare var Datasource: any;
export default Datasource;

View File

@ -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;
}); });

View File

@ -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
};
}); });

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,3 @@
declare var Datasource: any;
export default Datasource;

View File

@ -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;
});
}); });

View File

@ -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',

View File

@ -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;

View File

@ -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,
};
});

View File

@ -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>

View File

@ -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,

View File

@ -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;

View File

@ -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;
});
});

View File

@ -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'};
});
});

View File

@ -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
} }

View File

@ -0,0 +1,3 @@
declare var Datasource: any;
export default Datasource;

View File

@ -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;
});
}); });

View File

@ -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,
};
}); });

View File

@ -1,3 +1,2 @@
<div ng-include="httpConfigPartialSrc"></div> <datasource-http-settings></datasource-http-settings>

View File

@ -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,

View File

@ -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() {

View File

@ -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;
});
}); });

View File

@ -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
};
}); });

View File

@ -1,5 +1,4 @@
<div ng-include="httpConfigPartialSrc"></div> <datasource-http-settings></datasource-http-settings>
<br>
<h5>InfluxDB Details</h5> <h5>InfluxDB Details</h5>

View File

@ -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,

View File

@ -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;
});
});

View File

@ -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}

View File

@ -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
} }

View File

@ -0,0 +1,3 @@
declare var Datasource: any;
export default Datasource;

View File

@ -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;
}); });

View File

@ -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
};
}); });

View File

@ -1,4 +1,2 @@
<div ng-include="httpConfigPartialSrc"></div> <datasource-http-settings></datasource-http-settings>
<br>

View File

@ -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"

View File

@ -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');
});
});
});

View File

@ -0,0 +1,3 @@
declare var Datasource: any;
export default Datasource;

View File

@ -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;
});
}); });

View File

@ -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
};
}); });

View File

@ -1,4 +1,2 @@
<div ng-include="httpConfigPartialSrc"></div> <datasource-http-settings></datasource-http-settings>
<br>

View File

@ -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
} }

View File

@ -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;

View File

@ -1,18 +0,0 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.services');
module.factory('SqlDatasource', function() {
function SqlDatasource() {
}
return SqlDatasource;
});
});

View File

@ -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&nbsp;
<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>

View File

@ -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