diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go new file mode 100644 index 00000000000..13a9bc43f94 --- /dev/null +++ b/pkg/plugins/app_plugin.go @@ -0,0 +1,38 @@ +package plugins + +import ( + "encoding/json" + + "github.com/grafana/grafana/pkg/models" +) + +type AppPluginPage struct { + Text string `json:"text"` + Icon string `json:"icon"` + Url string `json:"url"` + ReqRole models.RoleType `json:"reqRole"` +} + +type AppPluginCss struct { + Light string `json:"light"` + Dark string `json:"dark"` +} + +type AppPlugin struct { + FrontendPluginBase + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Css *AppPluginCss `json:"css"` + Page *AppPluginPage `json:"page"` +} + +func (p *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error { + if err := decoder.Decode(&p); err != nil { + return err + } + + p.PluginDir = pluginDir + p.initFrontendPlugin() + Apps[p.Id] = p + return nil +} diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go new file mode 100644 index 00000000000..e1bb9047297 --- /dev/null +++ b/pkg/plugins/datasource_plugin.go @@ -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 +} diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go new file mode 100644 index 00000000000..1e20db1e7f5 --- /dev/null +++ b/pkg/plugins/frontend_plugin.go @@ -0,0 +1,47 @@ +package plugins + +import ( + "net/url" + "path" +) + +type FrontendPluginBase struct { + PluginBase + Module string `json:"module"` + StaticRoot string `json:"staticRoot"` +} + +func (fp *FrontendPluginBase) initFrontendPlugin() { + if fp.StaticRoot != "" { + StaticRoutes = append(StaticRoutes, &PluginStaticRoute{ + Directory: 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.Type, 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) +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 4516bbd5491..2ddbcf1dd83 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -1,15 +1,22 @@ package plugins import ( + "encoding/json" + "github.com/grafana/grafana/pkg/models" ) -type PluginCommon struct { - Type string `json:"type"` - Name string `json:"name"` - Id string `json:"id"` - StaticRoot string `json:"staticRoot"` - Info PluginInfo `json:"info"` +type PluginLoader interface { + Load(decoder *json.Decoder, pluginDir string) error +} + +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 PluginInfo struct { @@ -29,30 +36,11 @@ type PluginLogos struct { Large string `json:"large"` } -type DataSourcePlugin struct { - PluginCommon - Module string `json:"module"` - ServiceName string `json:"serviceName"` - Partials map[string]interface{} `json:"partials"` - DefaultMatchFormat string `json:"defaultMatchFormat"` - Annotations bool `json:"annotations"` - Metrics bool `json:"metrics"` - BuiltIn bool `json:"builtIn"` - Mixed bool `json:"mixed"` - App string `json:"app"` -} - type PluginStaticRoute struct { Directory string PluginId string } -type PanelPlugin struct { - PluginCommon - Module string `json:"module"` - App string `json:"app"` -} - type ApiPluginRoute struct { Path string `json:"path"` Method string `json:"method"` @@ -60,34 +48,11 @@ type ApiPluginRoute struct { ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"` ReqRole models.RoleType `json:"reqRole"` Url string `json:"url"` - App string `json:"app"` -} - -type AppPluginPage struct { - Text string `json:"text"` - Icon string `json:"icon"` - Url string `json:"url"` - ReqRole models.RoleType `json:"reqRole"` -} - -type AppPluginCss struct { - Light string `json:"light"` - Dark string `json:"dark"` } type ApiPlugin struct { - PluginCommon + PluginBase Routes []*ApiPluginRoute `json:"routes"` - App string `json:"app"` -} - -type AppPlugin struct { - PluginCommon - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Module string `json:"module"` - Css *AppPluginCss `json:"css"` - Page *AppPluginPage `json:"page"` } type EnabledPlugins struct { diff --git a/pkg/plugins/panel_plugin.go b/pkg/plugins/panel_plugin.go new file mode 100644 index 00000000000..5b99ac52344 --- /dev/null +++ b/pkg/plugins/panel_plugin.go @@ -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 +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 59b0b0db866..4626ad8df15 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -5,10 +5,10 @@ import ( "encoding/json" "errors" "io" - "net/url" "os" "path" "path/filepath" + "reflect" "strings" "text/template" @@ -24,6 +24,7 @@ var ( ApiPlugins map[string]*ApiPlugin StaticRoutes []*PluginStaticRoute Apps map[string]*AppPlugin + PluginTypes map[string]interface{} ) type PluginScanner struct { @@ -37,6 +38,12 @@ func Init() error { 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")) checkPluginPaths() @@ -115,27 +122,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro return nil } -func evalRelativePluginUrlPath(pathStr string, pluginId string) string { - u, _ := url.Parse(pathStr) - if u.IsAbs() { - return pathStr - } - return path.Join("public/plugins", pluginId, pathStr) -} - -func addPublicContent(plugin *PluginCommon, currentDir string) { - if plugin.StaticRoot != "" { - StaticRoutes = append(StaticRoutes, &PluginStaticRoute{ - Directory: path.Join(currentDir, plugin.StaticRoot), - PluginId: plugin.Id, - }) - } - - plugin.Info.Logos.Small = evalRelativePluginUrlPath(plugin.Info.Logos.Small, plugin.Id) - plugin.Info.Logos.Large = evalRelativePluginUrlPath(plugin.Info.Logos.Large, plugin.Id) -} - -func interpolatePluginJson(reader io.Reader, pluginCommon *PluginCommon) (io.Reader, error) { +func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) { buf := new(bytes.Buffer) buf.ReadFrom(reader) jsonStr := buf.String() // @@ -167,7 +154,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { defer reader.Close() jsonParser := json.NewDecoder(reader) - pluginCommon := PluginCommon{} + pluginCommon := PluginBase{} if err := jsonParser.Decode(&pluginCommon); err != nil { return err } @@ -177,52 +164,21 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { } reader.Seek(0, 0) - if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil { return err } else { jsonParser = json.NewDecoder(newReader) } - switch pluginCommon.Type { - case "datasource": - p := DataSourcePlugin{} - if err := jsonParser.Decode(&p); err != nil { - return err - } + var loader PluginLoader - DataSources[p.Id] = &p - addPublicContent(&p.PluginCommon, currentDir) - - case "panel": - p := PanelPlugin{} - reader.Seek(0, 0) - if err := jsonParser.Decode(&p); err != nil { - return err - } - - Panels[p.Id] = &p - addPublicContent(&p.PluginCommon, currentDir) - case "api": - p := ApiPlugin{} - reader.Seek(0, 0) - if err := jsonParser.Decode(&p); err != nil { - return err - } - ApiPlugins[p.Id] = &p - case "app": - p := AppPlugin{} - reader.Seek(0, 0) - if err := jsonParser.Decode(&p); err != nil { - return err - } - Apps[p.Id] = &p - addPublicContent(&p.PluginCommon, currentDir) - default: + 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) } func GetEnabledPlugins(orgApps []*models.AppPlugin) EnabledPlugins { diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index dc95d0e2b40..1808f72ec0d 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -19,6 +19,10 @@ 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() { diff --git a/public/app/plugins/datasource/cloudwatch/plugin.json b/public/app/plugins/datasource/cloudwatch/plugin.json index 68e0a8d1dae..49c5341bd21 100644 --- a/public/app/plugins/datasource/cloudwatch/plugin.json +++ b/public/app/plugins/datasource/cloudwatch/plugin.json @@ -3,13 +3,6 @@ "name": "CloudWatch", "id": "cloudwatch", - "module": "app/plugins/datasource/cloudwatch/module", - - "partials": { - "config": "app/plugins/datasource/cloudwatch/partials/config.html", - "query": "app/plugins/datasource/cloudwatch/partials/query.editor.html" - }, - "metrics": true, "annotations": true } diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index fecdb3ae7cc..7e975a7e93b 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -3,8 +3,6 @@ "name": "Elasticsearch", "id": "elasticsearch", - "module": "app/plugins/datasource/elasticsearch/module", - "defaultMatchFormat": "lucene", "annotations": true, "metrics": true diff --git a/public/app/plugins/datasource/grafana/plugin.json b/public/app/plugins/datasource/grafana/plugin.json index 4d4f55b647d..fdccb24b59d 100644 --- a/public/app/plugins/datasource/grafana/plugin.json +++ b/public/app/plugins/datasource/grafana/plugin.json @@ -3,8 +3,6 @@ "name": "Grafana", "id": "grafana", - "module": "app/plugins/datasource/grafana/module", - "builtIn": true, "metrics": true } diff --git a/public/app/plugins/datasource/graphite/module.js b/public/app/plugins/datasource/graphite/module.js index b7910448304..72dd9eeaf0a 100644 --- a/public/app/plugins/datasource/graphite/module.js +++ b/public/app/plugins/datasource/graphite/module.js @@ -19,6 +19,10 @@ function (angular, GraphiteDatasource) { 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, }; diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index 175ac5fa659..d2836b2a107 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -3,8 +3,6 @@ "type": "datasource", "id": "graphite", - "module": "app/plugins/datasource/graphite/module", - "defaultMatchFormat": "glob", "metrics": true, "annotations": true diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index 29d6a5a0f58..4007010fd21 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -3,8 +3,6 @@ "name": "InfluxDB 0.9.x", "id": "influxdb", - "module": "app/plugins/datasource/influxdb/module", - "defaultMatchFormat": "regex values", "metrics": true, "annotations": true diff --git a/public/app/plugins/datasource/mixed/plugin.json b/public/app/plugins/datasource/mixed/plugin.json index fb9bb340a04..b8c08446cb3 100644 --- a/public/app/plugins/datasource/mixed/plugin.json +++ b/public/app/plugins/datasource/mixed/plugin.json @@ -5,9 +5,5 @@ "builtIn": true, "mixed": true, - - "serviceName": "MixedDatasource", - - "module": "app/plugins/datasource/mixed/datasource", "metrics": true } diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index ec01b48c24b..82fe2c32062 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -3,8 +3,6 @@ "name": "OpenTSDB", "id": "opentsdb", - "module": "app/plugins/datasource/opentsdb/module", - "metrics": true, "defaultMatchFormat": "pipe" } diff --git a/public/app/plugins/datasource/prometheus/datasource.d.ts b/public/app/plugins/datasource/prometheus/datasource.d.ts new file mode 100644 index 00000000000..a50d7ca49cc --- /dev/null +++ b/public/app/plugins/datasource/prometheus/datasource.d.ts @@ -0,0 +1,3 @@ +declare var Datasource: any; +export default Datasource; + diff --git a/public/app/plugins/datasource/prometheus/datasource.js b/public/app/plugins/datasource/prometheus/datasource.js index 391affb0c8d..9560fe8aa5f 100644 --- a/public/app/plugins/datasource/prometheus/datasource.js +++ b/public/app/plugins/datasource/prometheus/datasource.js @@ -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; }); diff --git a/public/app/plugins/datasource/prometheus/directives.js b/public/app/plugins/datasource/prometheus/module.js similarity index 52% rename from public/app/plugins/datasource/prometheus/directives.js rename to public/app/plugins/datasource/prometheus/module.js index 2ceed8bffdb..91b8d498645 100644 --- a/public/app/plugins/datasource/prometheus/directives.js +++ b/public/app/plugins/datasource/prometheus/module.js @@ -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 + }; }); diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index 2580db9e5c9..4cd55605816 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -3,13 +3,5 @@ "name": "Prometheus", "id": "prometheus", - "serviceName": "PrometheusDatasource", - - "module": "app/plugins/datasource/prometheus/datasource", - - "partials": { - "config": "app/plugins/datasource/prometheus/partials/config.html" - }, - "metrics": true } diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index a03e39a9351..92620678eaf 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -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;