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/edit/:id", reqGrafanaAdmin, Index)
r.Get("/plugins", reqSignedIn, Index)
r.Get("/plugins/edit/*", reqSignedIn, Index)
r.Get("/apps", reqSignedIn, Index)
r.Get("/apps/edit/*", reqSignedIn, Index)
r.Get("/dashboard/*", reqSignedIn, Index)
r.Get("/dashboard-solo/*", reqSignedIn, Index)
@ -120,6 +120,11 @@ func Register(r *macaron.Macaron) {
r.Get("/invites", wrap(GetPendingOrgInvites))
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
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)
// create new org
@ -205,5 +210,7 @@ func Register(r *macaron.Macaron) {
// rendering
r.Get("/render/*", reqSignedIn, RenderToPng)
InitApiPluginRoutes(r)
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 (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
//"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"
@ -115,13 +116,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
}
func GetDataSourcePlugins(c *middleware.Context) {
dsList := make(map[string]interface{})
dsList := make(map[string]*plugins.DataSourcePlugin)
for key, value := range plugins.DataSources {
if !value.BuiltIn {
dsList[key] = value
if enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId); err != nil {
c.JsonApiErr(500, "Failed to get org apps", err)
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
GoogleTagManagerId string
PluginCss []*PluginCss
PluginJs []string
MainNavLinks []*NavLink
PluginCss []*PluginCss
PluginModules []string
MainNavLinks []*NavLink
}
type PluginCss struct {
@ -21,5 +21,6 @@ type PluginCss struct {
type NavLink struct {
Text string `json:"text"`
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{})
var defaultDatasource string
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
if err != nil {
return nil, err
}
for _, ds := range orgDataSources {
url := ds.Url
@ -42,7 +47,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"url": url,
}
meta, exists := plugins.DataSources[ds.Type]
meta, exists := enabledPlugins.DataSources[ds.Type]
if !exists {
log.Error(3, "Could not find plugin definition for data source: %v", ds.Type)
continue
@ -110,8 +115,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
}
panels := map[string]interface{}{}
for _, panel := range plugins.Panels {
panels[panel.Type] = map[string]interface{}{
for _, panel := range enabledPlugins.Panels {
panels[panel.Id] = map[string]interface{}{
"module": panel.Module,
"name": panel.Name,
}

View File

@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"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{
Text: "Dashboards",
Icon: "fa fa-fw fa-th-large",
Href: "/",
Url: "/",
})
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{
Text: "Data Sources",
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

View File

@ -30,9 +30,9 @@ func newMacaron() *macaron.Macaron {
}
for _, route := range plugins.StaticRoutes {
pluginRoute := path.Join("/public/plugins/", route.Url)
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Path)
mapStatic(m, route.Path, "", pluginRoute)
pluginRoute := path.Join("/public/plugins/", route.PluginId)
log.Info("Plugin: Adding static route %s -> %s", pluginRoute, route.Directory)
mapStatic(m, route.Directory, "", pluginRoute)
}
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)
}
func (ctx *Context) HasUserRole(role m.RoleType) bool {
return ctx.OrgRole.Includes(role)
}

View File

@ -2,11 +2,12 @@ package models
import "time"
type PluginBundle struct {
type AppSettings struct {
Id int64
Type string
AppId string
OrgId int64
Enabled bool
Pinned bool
JsonData map[string]interface{}
Created time.Time
@ -17,18 +18,18 @@ type PluginBundle struct {
// COMMANDS
// Also acts as api DTO
type UpdatePluginBundleCmd struct {
Type string `json:"type" binding:"Required"`
type UpdateAppSettingsCmd struct {
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
JsonData map[string]interface{} `json:"jsonData"`
Id int64 `json:"-"`
OrgId int64 `json:"-"`
AppId string `json:"-"`
OrgId int64 `json:"-"`
}
// ---------------------
// QUERIES
type GetPluginBundlesQuery struct {
type GetAppSettingsQuery struct {
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
}
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 {
Id 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
type DataSourcePlugin struct {
Type string `json:"type"`
Name string `json:"name"`
ServiceName string `json:"serviceName"`
Module string `json:"module"`
Partials map[string]interface{} `json:"partials"`
DefaultMatchFormat string `json:"defaultMatchFormat"`
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
BuiltIn bool `json:"builtIn"`
StaticRootConfig *StaticRootConfig `json:"staticRoot"`
import (
"encoding/json"
"github.com/grafana/grafana/pkg/models"
)
type PluginLoader interface {
Load(decoder *json.Decoder, pluginDir string) error
}
type PanelPlugin struct {
Type string `json:"type"`
Name string `json:"name"`
Module string `json:"module"`
StaticRootConfig *StaticRootConfig `json:"staticRoot"`
type PluginBase struct {
Type string `json:"type"`
Name string `json:"name"`
Id string `json:"id"`
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"`
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
import (
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"text/template"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
@ -14,9 +18,12 @@ import (
)
var (
DataSources map[string]DataSourcePlugin
Panels map[string]PanelPlugin
StaticRoutes []*StaticRootConfig
DataSources map[string]*DataSourcePlugin
Panels map[string]*PanelPlugin
ApiPlugins map[string]*ApiPlugin
StaticRoutes []*PluginStaticRoute
Apps map[string]*AppPlugin
PluginTypes map[string]interface{}
)
type PluginScanner struct {
@ -25,18 +32,45 @@ type PluginScanner struct {
}
func Init() error {
DataSources = make(map[string]DataSourcePlugin)
StaticRoutes = make([]*StaticRootConfig, 0)
Panels = make(map[string]PanelPlugin)
DataSources = make(map[string]*DataSourcePlugin)
ApiPlugins = make(map[string]*ApiPlugin)
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.PluginsPath))
checkExternalPluginPaths()
checkPluginPaths()
// checkDependencies()
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() {
if strings.HasPrefix(section.Name(), "plugin.") {
path := section.Key("path").String()
@ -87,11 +121,26 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
return nil
}
func addStaticRoot(staticRootConfig *StaticRootConfig, currentDir string) {
if staticRootConfig != nil {
staticRootConfig.Path = path.Join(currentDir, staticRootConfig.Path)
StaticRoutes = append(StaticRoutes, staticRootConfig)
func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) {
buf := new(bytes.Buffer)
buf.ReadFrom(reader)
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 {
@ -104,46 +153,29 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
defer reader.Close()
jsonParser := json.NewDecoder(reader)
pluginJson := make(map[string]interface{})
if err := jsonParser.Decode(&pluginJson); err != nil {
pluginCommon := PluginBase{}
if err := jsonParser.Decode(&pluginCommon); err != nil {
return err
}
pluginType, exists := pluginJson["pluginType"]
if !exists {
return errors.New("Did not find pluginType property in plugin.json")
if pluginCommon.Id == "" || pluginCommon.Type == "" {
return errors.New("Did not find type and id property in plugin.json")
}
if pluginType == "datasource" {
p := DataSourcePlugin{}
reader.Seek(0, 0)
if err := jsonParser.Decode(&p); err != nil {
return err
}
if p.Type == "" {
return errors.New("Did not find type property in plugin.json")
}
DataSources[p.Type] = p
addStaticRoot(p.StaticRootConfig, currentDir)
reader.Seek(0, 0)
if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil {
return err
} else {
jsonParser = json.NewDecoder(newReader)
}
if pluginType == "panel" {
p := PanelPlugin{}
reader.Seek(0, 0)
if err := jsonParser.Decode(&p); err != nil {
return err
}
var loader PluginLoader
if p.Type == "" {
return errors.New("Did not find type property in plugin.json")
}
Panels[p.Type] = p
addStaticRoot(p.StaticRootConfig, currentDir)
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
return errors.New("Unkown plugin type " + pluginCommon.Type)
} else {
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
}
return nil
return loader.Load(jsonParser, currentDir)
}

View File

@ -18,5 +18,22 @@ func TestPluginScans(t *testing.T) {
So(err, ShouldBeNil)
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"
func addPluginBundleMigration(mg *Migrator) {
func addAppSettingsMigration(mg *Migrator) {
var pluginBundleV1 = Table{
Name: "plugin_bundle",
appSettingsV1 := Table{
Name: "app_settings",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: 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: "pinned", Type: DB_Bool, Nullable: false},
{Name: "json_data", Type: DB_Text, Nullable: true},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
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 ------------------
addTableIndicesMigrations(mg, "v1", pluginBundleV1)
addTableIndicesMigrations(mg, "v3", appSettingsV1)
}

View File

@ -18,7 +18,7 @@ func AddMigrations(mg *Migrator) {
addApiKeyMigrations(mg)
addDashboardSnapshotMigrations(mg)
addQuotaMigration(mg)
addPluginBundleMigration(mg)
addAppSettingsMigration(mg)
addSessionMigration(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([
'./grafana_ctrl',
'./search_ctrl',

View File

@ -19,7 +19,8 @@ function (angular, _, $, coreModule, config) {
$scope.mainLinks.push({
text: item.text,
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">' +
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-checked="' + model + '"></input>' +
' <label for="' + scope.$id + model + '" class="cr1"></label>';
template = label + template;
template = template + label;
elem.replaceWith($compile(angular.element(template))(scope));
}
};

View File

@ -10,6 +10,7 @@ define([
$locationProvider.html5Mode(true);
var loadOrgBundle = new BundleLoader.BundleLoader('app/features/org/all');
var loadAppsBundle = new BundleLoader.BundleLoader('app/features/apps/all');
$routeProvider
.when('/', {
@ -41,17 +42,17 @@ define([
controller : 'DashboardImportCtrl',
})
.when('/datasources', {
templateUrl: 'app/features/org/partials/datasources.html',
templateUrl: 'app/features/datasources/partials/list.html',
controller : 'DataSourcesCtrl',
resolve: loadOrgBundle,
})
.when('/datasources/edit/:id', {
templateUrl: 'app/features/org/partials/datasourceEdit.html',
templateUrl: 'app/features/datasources/partials/edit.html',
controller : 'DataSourceEditCtrl',
resolve: loadOrgBundle,
})
.when('/datasources/new', {
templateUrl: 'app/features/org/partials/datasourceEdit.html',
templateUrl: 'app/features/datasources/partials/edit.html',
controller : 'DataSourceEditCtrl',
resolve: loadOrgBundle,
})
@ -131,15 +132,17 @@ define([
templateUrl: 'app/partials/reset_password.html',
controller : 'ResetPasswordCtrl',
})
.when('/plugins', {
templateUrl: 'app/features/org/partials/plugins.html',
controller: 'PluginsCtrl',
resolve: loadOrgBundle,
.when('/apps', {
templateUrl: 'app/features/apps/partials/list.html',
controller: 'AppListCtrl',
controllerAs: 'ctrl',
resolve: loadAppsBundle,
})
.when('/plugins/edit/:type', {
templateUrl: 'app/features/org/partials/pluginEdit.html',
controller: 'PluginEditCtrl',
resolve: loadOrgBundle,
.when('/apps/edit/:appId', {
templateUrl: 'app/features/apps/partials/edit.html',
controller: 'AppEditCtrl',
controllerAs: 'ctrl',
resolve: loadAppsBundle,
})
.when('/global-alerts', {
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',

View File

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

View File

@ -7,7 +7,7 @@ define([
function (angular, _, coreModule, config) {
'use strict';
coreModule.default.service('datasourceSrv', function($q, $injector) {
coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope) {
var self = this;
this.init = function() {
@ -58,18 +58,27 @@ function (angular, _, coreModule, config) {
}
var deferred = $q.defer();
var pluginDef = dsConfig.meta;
System.import(pluginDef.module).then(function() {
var AngularService = $injector.get(pluginDef.serviceName);
var instance = new AngularService(dsConfig, pluginDef);
System.import(pluginDef.module).then(function(plugin) {
// check if its in cache now
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.name = name;
self.datasources[name] = instance;
deferred.resolve(instance);
}).catch(function(err) {
console.log('Failed to load data source: ' + err);
$rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]);
});
return deferred.promise;

View File

@ -44,7 +44,7 @@
<table class="grafana-options-table">
<tr ng-repeat="annotation in annotations">
<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}}
</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) {
var editScope = $rootScope.$new();
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 datasourceTypes = [];
module.directive('datasourceHttpSettings', function() {
return {templateUrl: 'app/features/datasources/partials/http_settings.html'};
});
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: {}};
$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.isNew = true;
$scope.datasources = [];
@ -59,7 +47,7 @@ function (angular, _, config) {
backendSrv.get('/api/datasources/' + id).then(function(ds) {
$scope.isNew = false;
$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();
});
});

View File

@ -42,7 +42,7 @@
<div class="clearfix"></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">
<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>

View File

@ -1,13 +1,8 @@
define([
'./datasourcesCtrl',
'./datasourceEditCtrl',
'./orgUsersCtrl',
'./newOrgCtrl',
'./userInviteCtrl',
'./orgApiKeysCtrl',
'./orgDetailsCtrl',
'./pluginsCtrl',
'./pluginEditCtrl',
'./plugin_srv',
'./plugin_directive',
'../datasources/all',
], 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) {
var self = this;
@ -62,12 +89,26 @@ function (angular, $, config) {
editorScope = options.scope.$new();
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) {
return {
restrict: 'E',
@ -90,7 +131,7 @@ function (angular, $, config) {
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);
$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) {
return {
restrict: 'E',

View File

@ -34,13 +34,6 @@
</ul>
</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">
<div class="sidemenu-system-section-inner">
<i class="fa fa-fw fa-cubes"></i>
@ -52,8 +45,11 @@
</li>
<li ng-repeat="item in mainLinks">
<a href="{{item.href}}" class="sidemenu-item sidemenu-main-link" target="{{item.target}}">
<span class="icon-circle sidemenu-icon"><i class="{{item.icon}}"></i></span>
<a href="{{item.url}}" class="sidemenu-item sidemenu-main-link" target="{{item.target}}">
<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>
</a>
</li>

View File

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

View File

@ -4,24 +4,19 @@ define([
'moment',
'app/core/utils/datemath',
'./query_ctrl',
'./directives',
],
function (angular, _, moment, dateMath) {
'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) {
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) {
this.query = function(options) {
var start = convertToCloudWatchTime(options.range.from, false);
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({
region: query.region,
action: 'GetMetricStatistics',
@ -88,15 +83,15 @@ function (angular, _, moment, dateMath) {
});
};
CloudWatchDatasource.prototype.getRegions = function() {
this.getRegions = function() {
return this.awsRequest({action: '__GetRegions'});
};
CloudWatchDatasource.prototype.getNamespaces = function() {
this.getNamespaces = function() {
return this.awsRequest({action: '__GetNamespaces'});
};
CloudWatchDatasource.prototype.getMetrics = function(namespace) {
this.getMetrics = function(namespace) {
return this.awsRequest({
action: '__GetMetrics',
parameters: {
@ -105,7 +100,7 @@ function (angular, _, moment, dateMath) {
});
};
CloudWatchDatasource.prototype.getDimensionKeys = function(namespace) {
this.getDimensionKeys = function(namespace) {
return this.awsRequest({
action: '__GetDimensions',
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 = {
region: templateSrv.replace(region),
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({
region: region,
action: 'DescribeInstances',
@ -149,7 +144,7 @@ function (angular, _, moment, dateMath) {
});
};
CloudWatchDatasource.prototype.metricFindQuery = function(query) {
this.metricFindQuery = function(query) {
var region;
var namespace;
var metricName;
@ -210,7 +205,7 @@ function (angular, _, moment, dateMath) {
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({
region: region,
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({
region: region,
action: 'DescribeAlarmHistory',
@ -226,7 +221,7 @@ function (angular, _, moment, dateMath) {
});
};
CloudWatchDatasource.prototype.annotationQuery = function(options) {
this.annotationQuery = function(options) {
var annotation = options.annotation;
var region = templateSrv.replace(annotation.region);
var namespace = templateSrv.replace(annotation.namespace);
@ -278,7 +273,7 @@ function (angular, _, moment, dateMath) {
return d.promise;
};
CloudWatchDatasource.prototype.testDatasource = function() {
this.testDatasource = function() {
/* use billing metrics for test */
var region = this.defaultRegion;
var namespace = 'AWS/Billing';
@ -290,7 +285,7 @@ function (angular, _, moment, dateMath) {
});
};
CloudWatchDatasource.prototype.awsRequest = function(data) {
this.awsRequest = function(data) {
var options = {
method: 'POST',
url: this.proxyUrl,
@ -302,7 +297,7 @@ function (angular, _, moment, dateMath) {
});
};
CloudWatchDatasource.prototype.getDefaultRegion = function() {
this.getDefaultRegion = function() {
return this.defaultRegion;
};
@ -361,7 +356,7 @@ function (angular, _, moment, dateMath) {
});
}
return CloudWatchDatasource;
});
}
return CloudWatchDatasource;
});

View File

@ -1,8 +1,9 @@
define([
'angular',
'./datasource',
'./query_parameter_ctrl',
],
function (angular) {
function (angular, CloudWatchDatasource) {
'use strict';
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",
"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"
},
"id": "cloudwatch",
"metrics": true,
"annotations": true

View File

@ -3,25 +3,25 @@ import "../datasource";
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
describe('CloudWatchDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings = {
jsonData: {defaultRegion: 'us-east-1', access: 'proxy'},
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
beforeEach(ctx.createService('CloudWatchDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({
jsonData: {
defaultRegion: 'us-east-1',
access: 'proxy'
}
});
});
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 CloudWatch query', function() {
var requestParams;

View File

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

View File

@ -7,33 +7,27 @@ define([
'./index_pattern',
'./elastic_response',
'./query_ctrl',
'./directives'
],
function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) {
'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) {
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) {
this._request = function(method, url, data) {
var options = {
url: this.url + "/" + url,
method: method,
@ -52,21 +46,21 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
return backendSrv.datasourceRequest(options);
};
ElasticDatasource.prototype._get = function(url) {
this._get = function(url) {
return this._request('GET', this.indexPattern.getIndexForToday() + url)
.then(function(results) {
return results.data;
});
.then(function(results) {
return results.data;
});
};
ElasticDatasource.prototype._post = function(url, data) {
this._post = function(url, data) {
return this._request('POST', url, data)
.then(function(results) {
return results.data;
});
.then(function(results) {
return results.data;
});
};
ElasticDatasource.prototype.annotationQuery = function(options) {
this.annotationQuery = function(options) {
var annotation = options.annotation;
var timeField = annotation.timeField || '@timestamp';
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 { status: "success", message: "Data source is working", title: "Success" };
}, 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};
header.index = this.indexPattern.getIndexList(timeFrom, timeTo);
return angular.toJson(header);
};
ElasticDatasource.prototype.query = function(options) {
this.query = function(options) {
var payload = "";
var target;
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) {
var fields = {};
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 header = this.getQueryHeader('count', range.from, range.to);
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 = angular.fromJson(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)
.then(function(result) {
return angular.fromJson(result._source.dashboard);
});
};
ElasticDatasource.prototype.searchDashboards = function() {
this.searchDashboards = function() {
var query = {
query: { query_string: { query: '*' } },
size: 10000,
@ -308,7 +302,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
return displayHits;
});
};
}
return ElasticDatasource;
});
return ElasticDatasource;
});

View File

@ -20,6 +20,10 @@ function (angular) {
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() {
return {
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>
<br>
<datasource-http-settings></datasource-http-settings>
<h5>Elasticsearch details</h5>
@ -42,8 +41,8 @@
</ul>
<div class="clearfix"></div>
</div>
</div>
<br>
<h5>Default query settings</h5>
<div class="tight-form last">
@ -53,7 +52,7 @@
</li>
<li>
<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 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>

View File

@ -1,16 +1,7 @@
{
"pluginType": "datasource",
"type": "datasource",
"name": "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"
},
"id": "elasticsearch",
"defaultMatchFormat": "lucene",
"annotations": true,

View File

@ -1,28 +1,32 @@
import "../datasource";
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import angular from 'angular';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
describe('ElasticDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings: any = {jsonData: {}};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
beforeEach(ctx.createService('ElasticDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({jsonData: {}});
});
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
ctx.$q = $q;
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() {
beforeEach(function() {
ctx.ds = new ctx.service({
url: 'http://es.com',
index: '[asd-]YYYY.MM.DD',
jsonData: { interval: 'Daily' }
});
createDatasource({url: 'http://es.com', index: '[asd-]YYYY.MM.DD', jsonData: {interval: 'Daily'}});
});
it('should translate index pattern to current day', function() {
@ -44,11 +48,7 @@ describe('ElasticDatasource', function() {
var requestOptions, parts, header;
beforeEach(function() {
ctx.ds = new ctx.service({
url: 'http://es.com',
index: '[asd-]YYYY.MM.DD',
jsonData: { interval: 'Daily' }
});
createDatasource({url: 'http://es.com', index: '[asd-]YYYY.MM.DD', jsonData: {interval: 'Daily'}});
ctx.backendSrv.datasourceRequest = function(options) {
requestOptions = options;
@ -83,7 +83,7 @@ describe('ElasticDatasource', function() {
var requestOptions, parts, header;
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) {
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",
"id": "grafana",
"builtIn": true,
"type": "grafana",
"serviceName": "GrafanaDatasource",
"module": "app/plugins/datasource/grafana/datasource",
"metrics": true
}

View File

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

View File

@ -4,7 +4,6 @@ define([
'jquery',
'app/core/config',
'app/core/utils/datemath',
'./directives',
'./query_ctrl',
'./func_editor',
'./add_graphite_func',
@ -12,20 +11,16 @@ define([
function (angular, _, $, config, dateMath) {
'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) {
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) {
this.query = function(options) {
try {
var graphOptions = {
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 []; }
for (var i = 0; i < result.data.length; i++) {
var series = result.data[i];
@ -73,7 +68,7 @@ function (angular, _, $, config, dateMath) {
return result;
};
GraphiteDatasource.prototype.annotationQuery = function(options) {
this.annotationQuery = function(options) {
// Graphite metric as annotation
if (options.annotation.target) {
var target = templateSrv.replace(options.annotation.target);
@ -85,50 +80,49 @@ function (angular, _, $, config, dateMath) {
};
return this.query(graphiteQuery)
.then(function(result) {
var list = [];
.then(function(result) {
var list = [];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
list.push({
annotation: options.annotation,
time: datapoint[1],
title: target.target
});
}
list.push({
annotation: options.annotation,
time: datapoint[1],
title: target.target
});
}
}
return list;
});
return list;
});
}
// Graphite event as annotation
else {
var tags = templateSrv.replace(options.annotation.tags);
return this.events({range: options.rangeRaw, tags: tags})
.then(function(results) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
list.push({
annotation: options.annotation,
time: e.when * 1000,
title: e.what,
tags: e.tags,
text: e.data
});
}
return list;
});
list.push({
annotation: options.annotation,
time: e.when * 1000,
title: e.what,
tags: e.tags,
text: e.data
});
}
return list;
});
}
};
GraphiteDatasource.prototype.events = function(options) {
this.events = function(options) {
try {
var 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 (date === 'now') {
return 'now';
@ -178,7 +172,7 @@ function (angular, _, $, config, dateMath) {
return date.unix();
};
GraphiteDatasource.prototype.metricFindQuery = function(query) {
this.metricFindQuery = function(query) {
var interpolated;
try {
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 { 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 || ''} })
.then(function(results) {
return results.data.dashboards;
});
};
GraphiteDatasource.prototype.loadDashboard = function(dashName) {
this.loadDashboard = function(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) {
options.withCredentials = true;
}
@ -230,9 +224,9 @@ function (angular, _, $, config, dateMath) {
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 clean_options = [], targets = {};
var target, targetValue, i;
@ -296,9 +290,7 @@ function (angular, _, $, config, dateMath) {
return clean_options;
};
}
return GraphiteDatasource;
});
return GraphiteDatasource;
});

View File

@ -1,7 +1,8 @@
define([
'angular',
'./datasource',
],
function (angular) {
function (angular, GraphiteDatasource) {
'use strict';
var module = angular.module('grafana.directives');
@ -18,4 +19,11 @@ function (angular) {
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",
"type": "graphite",
"serviceName": "GraphiteDatasource",
"module": "app/plugins/datasource/graphite/datasource",
"partials": {
"config": "app/plugins/datasource/graphite/partials/config.html"
},
"type": "datasource",
"id": "graphite",
"defaultMatchFormat": "glob",
"metrics": true,

View File

@ -1,19 +1,24 @@
import "../datasource";
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
describe('graphiteDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings: any = {url: ['']};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
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() {
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() {

View File

@ -4,7 +4,6 @@ define([
'app/core/utils/datemath',
'./influx_series',
'./influx_query',
'./directives',
'./query_ctrl',
],
function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
@ -12,27 +11,22 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
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.type = 'influxdb';
this.urls = _.map(datasource.url.split(','), function(url) {
return url.trim();
});
this.supportAnnotations = true;
this.supportMetrics = true;
this.username = datasource.username;
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) {
this.query = function(options) {
var timeFilter = getTimeFilter(options);
var queryTargets = [];
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 query = options.annotation.query.replace('$timeFilter', timeFilter);
query = templateSrv.replace(query);
@ -106,7 +100,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
});
};
InfluxDatasource.prototype.metricFindQuery = function (query) {
this.metricFindQuery = function (query) {
var interpolated;
try {
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'});
};
InfluxDatasource.prototype.testDatasource = function() {
this.testDatasource = function() {
return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
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 currentUrl = self.urls.shift();
@ -219,9 +213,8 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
}
return (date.valueOf() / 1000).toFixed(0) + 's';
}
}
return InfluxDatasource;
});
return InfluxDatasource;
});

View File

@ -1,7 +1,8 @@
define([
'angular',
'./datasource',
],
function (angular) {
function (angular, InfluxDatasource) {
'use strict';
var module = angular.module('grafana.directives');
@ -18,4 +19,11 @@ function (angular) {
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>
<br>
<datasource-http-settings></datasource-http-settings>
<h5>InfluxDB Details</h5>

View File

@ -1,15 +1,7 @@
{
"pluginType": "datasource",
"type": "datasource",
"name": "InfluxDB 0.9.x",
"type": "influxdb",
"serviceName": "InfluxDatasource",
"module": "app/plugins/datasource/influxdb/datasource",
"partials": {
"config": "app/plugins/datasource/influxdb/partials/config.html"
},
"id": "influxdb",
"defaultMatchFormat": "regex values",
"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",
"id": "mixed",
"builtIn": true,
"mixed": true,
"type": "mixed",
"serviceName": "MixedDatasource",
"module": "app/plugins/datasource/mixed/datasource",
"metrics": true
}

View File

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

View File

@ -3,25 +3,19 @@ define([
'lodash',
'app/core/utils/datemath',
'moment',
'./directives',
'./queryCtrl',
],
function (angular, _, dateMath) {
'use strict';
var module = angular.module('grafana.services');
module.factory('OpenTSDBDatasource', function($q, backendSrv, templateSrv) {
function OpenTSDBDatasource(datasource) {
this.type = 'opentsdb';
this.url = datasource.url;
this.name = datasource.name;
this.supportMetrics = true;
}
function OpenTSDBDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.type = 'opentsdb';
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.supportMetrics = true;
// Called once per panel (graph)
OpenTSDBDatasource.prototype.query = function(options) {
this.query = function(options) {
var start = convertToTSDBTime(options.rangeRaw.from, false);
var end = convertToTSDBTime(options.rangeRaw.to, true);
var qs = [];
@ -60,7 +54,7 @@ function (angular, _, dateMath) {
});
};
OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
this.performTimeSeriesQuery = function(queries, start, end) {
var reqBody = {
start: start,
queries: queries
@ -80,13 +74,13 @@ function (angular, _, dateMath) {
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 result.data;
});
};
OpenTSDBDatasource.prototype._performMetricKeyValueLookup = function(metric, key) {
this._performMetricKeyValueLookup = function(metric, key) {
if(!metric || !key) {
return $q.when([]);
}
@ -105,7 +99,7 @@ function (angular, _, dateMath) {
});
};
OpenTSDBDatasource.prototype._performMetricKeyLookup = function(metric) {
this._performMetricKeyLookup = function(metric) {
if(!metric) { return $q.when([]); }
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({
method: 'GET',
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([]); }
var interpolated;
@ -181,14 +175,14 @@ function (angular, _, dateMath) {
return $q.when([]);
};
OpenTSDBDatasource.prototype.testDatasource = function() {
this.testDatasource = function() {
return this._performSuggestQuery('cpu', 'metrics').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
var aggregatorsPromise = null;
OpenTSDBDatasource.prototype.getAggregators = function() {
this.getAggregators = function() {
if (aggregatorsPromise) { return aggregatorsPromise; }
aggregatorsPromise = this._get('/api/aggregators').then(function(result) {
@ -311,7 +305,7 @@ function (angular, _, dateMath) {
return date.valueOf();
}
return OpenTSDBDatasource;
});
}
return OpenTSDBDatasource;
});

View File

@ -1,7 +1,8 @@
define([
'angular',
'./datasource',
],
function (angular) {
function (angular, OpenTsDatasource) {
'use strict';
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>
<br>
<datasource-http-settings></datasource-http-settings>

View File

@ -1,15 +1,7 @@
{
"pluginType": "datasource",
"type": "datasource",
"name": "OpenTSDB",
"type": "opentsdb",
"serviceName": "OpenTSDBDatasource",
"module": "app/plugins/datasource/opentsdb/datasource",
"partials": {
"config": "app/plugins/datasource/opentsdb/partials/config.html"
},
"id": "opentsdb",
"metrics": true,
"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',
'moment',
'app/core/utils/datemath',
'./directives',
'./query_ctrl',
],
function (angular, _, moment, dateMath) {
'use strict';
var module = angular.module('grafana.services');
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.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) {
this._request = function(method, url) {
var options = {
url: this.url + url,
method: method
@ -46,7 +40,7 @@ function (angular, _, moment, dateMath) {
};
// Called once per panel (graph)
PrometheusDatasource.prototype.query = function(options) {
this.query = function(options) {
var start = getPrometheusTime(options.range.from, false);
var end = getPrometheusTime(options.range.to, true);
@ -86,31 +80,31 @@ function (angular, _, moment, dateMath) {
var self = this;
return $q.all(allQueryPromise)
.then(function(allResponse) {
var result = [];
.then(function(allResponse) {
var result = [];
_.each(allResponse, function(response, index) {
if (response.status === 'error') {
self.lastErrors.query = response.error;
throw response.error;
}
delete self.lastErrors.query;
_.each(allResponse, function(response, index) {
if (response.status === 'error') {
self.lastErrors.query = response.error;
throw response.error;
}
delete self.lastErrors.query;
_.each(response.data.data.result, function(metricData) {
result.push(transformMetricData(metricData, options.targets[index]));
});
_.each(response.data.data.result, function(metricData) {
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;
return this._request('GET', url);
};
PrometheusDatasource.prototype.performSuggestQuery = function(query) {
this.performSuggestQuery = function(query) {
var url = '/api/v1/label/__name__/values';
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([]); }
var interpolated;
@ -196,7 +190,7 @@ function (angular, _, moment, dateMath) {
}
};
PrometheusDatasource.prototype.testDatasource = function() {
this.testDatasource = function() {
return this.metricFindQuery('metrics(.*)').then(function() {
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 PrometheusDatasource;
});
return PrometheusDatasource;
});

View File

@ -1,7 +1,8 @@
define([
'angular',
'./datasource',
],
function (angular) {
function (angular, PromDatasource) {
'use strict';
var module = angular.module('grafana.directives');
@ -10,4 +11,11 @@ function (angular) {
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>
<br>
<datasource-http-settings></datasource-http-settings>

View File

@ -1,15 +1,7 @@
{
"pluginType": "datasource",
"type": "datasource",
"name": "Prometheus",
"type": "prometheus",
"serviceName": "PrometheusDatasource",
"module": "app/plugins/datasource/prometheus/datasource",
"partials": {
"config": "app/plugins/datasource/prometheus/partials/config.html"
},
"id": "prometheus",
"metrics": true
}

View File

@ -1,17 +1,20 @@
import '../datasource';
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import Datasource from '../datasource';
describe('PrometheusDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings = {url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' };
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.createService('PrometheusDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({ url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' });
});
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 querying prometheus with one target using query editor target spec', function() {
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