A lot of work on backend plugin model for frontend components, right now for data sources, will enable dropin plugins for data sources and panels, #1472

This commit is contained in:
Torkel Ödegaard 2015-02-27 22:29:00 +01:00
parent 5bd5713a52
commit c198242292
42 changed files with 159 additions and 249 deletions

View File

@ -40,7 +40,7 @@ type DataSource struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
Name string `json:"name"`
Type m.DsType `json:"type"`
Type string `json:"type"`
Access m.DsAccess `json:"access"`
Url string `json:"url"`
Password string `json:"password"`

View File

@ -6,6 +6,7 @@ import (
"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"
"github.com/grafana/grafana/pkg/setting"
)
@ -38,6 +39,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"url": url,
}
meta, exists := plugins.DataSources[ds.Type]
if !exists {
//return nil, errors.New(fmt.Sprintf("Could not find plugin definition for data source: %v", ds.Type))
}
dsMap["meta"] = meta
if ds.IsDefault {
defaultDatasource = ds.Name
}

View File

@ -103,7 +103,7 @@ func createDataSource(c *cli.Context) {
OrgId: orgId,
Name: ds,
Url: url,
Type: m.DsType(dsType),
Type: dsType,
Access: m.DsAccess(dsAccess),
IsDefault: dsDefault,
}

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
@ -74,6 +75,7 @@ func runWeb(c *cli.Context) {
social.NewOAuthService()
eventpublisher.Init()
plugins.Init()
var err error
m := newMacaron()

View File

@ -20,7 +20,6 @@ var (
ErrDataSourceNotFound = errors.New("Data source not found")
)
type DsType string
type DsAccess string
type DataSource struct {
@ -29,7 +28,7 @@ type DataSource struct {
Version int
Name string
Type DsType
Type string
Access DsAccess
Url string
Password string
@ -51,7 +50,7 @@ type DataSource struct {
type AddDataSourceCommand struct {
OrgId int64 `json:"-"`
Name string
Type DsType
Type string
Access DsAccess
Url string
Password string
@ -67,7 +66,7 @@ type UpdateDataSourceCommand struct {
Id int64
OrgId int64
Name string
Type DsType
Type string
Access DsAccess
Url string
Password string

View File

@ -4,9 +4,11 @@ import (
"encoding/json"
"errors"
"os"
"path"
"path/filepath"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
type PluginMeta struct {
@ -15,7 +17,7 @@ type PluginMeta struct {
}
var (
List []*PluginMeta
DataSources map[string]interface{}
)
type PluginScanner struct {
@ -23,8 +25,12 @@ type PluginScanner struct {
errors []error
}
func Scan(pluginDir string) error {
List = make([]*PluginMeta, 0)
func Init() {
scan(path.Join(setting.StaticRootPath, "app/plugins"))
}
func scan(pluginDir string) error {
DataSources = make(map[string]interface{})
scanner := &PluginScanner{
pluginPath: pluginDir,
@ -51,31 +57,43 @@ func (scanner *PluginScanner) walker(path string, f os.FileInfo, err error) erro
}
if f.Name() == "plugin.json" {
pluginMeta, err := loadPluginMeta(path)
err := scanner.loadPluginJson(path)
if err != nil {
log.Error(3, "Failed to load plugin json file: %v, err: %v", path, err)
scanner.errors = append(scanner.errors, err)
} else {
List = append(List, pluginMeta)
}
}
return nil
}
func loadPluginMeta(path string) (*PluginMeta, error) {
func (scanner *PluginScanner) loadPluginJson(path string) error {
reader, err := os.Open(path)
if err != nil {
return nil, err
return err
}
defer reader.Close()
jsonParser := json.NewDecoder(reader)
pluginMeta := &PluginMeta{}
if err := jsonParser.Decode(pluginMeta); err != nil {
return nil, err
pluginJson := make(map[string]interface{})
if err := jsonParser.Decode(&pluginJson); err != nil {
return err
}
return pluginMeta, nil
pluginType, exists := pluginJson["pluginType"]
if !exists {
return errors.New("Did not find pluginType property in plugin.json")
}
if pluginType == "datasource" {
datasourceType, exists := pluginJson["type"]
if !exists {
return errors.New("Did not find type property in plugin.json")
}
DataSources[datasourceType.(string)] = pluginJson
}
return nil
}

View File

@ -11,9 +11,9 @@ func TestPluginScans(t *testing.T) {
Convey("When scaning for plugins", t, func() {
path, _ := filepath.Abs("../../src/app/plugins")
err := Scan(path)
err := scan(path)
So(err, ShouldBeNil)
So(len(List), ShouldEqual, 1)
So(len(DataSources), ShouldEqual, 1)
})
}

View File

@ -97,10 +97,6 @@ function (angular, $, _, appLevelRequire, config) {
'routes/backend/all',
];
_.each(config.plugins.dependencies, function(dep) {
preBootRequires.push('../plugins/' + dep);
});
app.boot = function() {
require(preBootRequires, function () {

View File

@ -52,14 +52,6 @@ function (_) {
// if (datasource.type === 'influxdb') { parseMultipleHosts(datasource); }
// });
if (settings.plugins.panels) {
_.extend(settings.panels, settings.plugins.panels);
}
if (!settings.plugins.dependencies) {
settings.plugins.dependencies = [];
}
return settings;
};
});

View File

@ -57,12 +57,10 @@ function (angular, _, config) {
$scope.setDatasource = function(datasource) {
$scope.panel.datasource = datasource;
$scope.datasource = datasourceSrv.get(datasource);
if (!$scope.datasource) {
$scope.panelMeta.error = "Cannot find datasource " + datasource;
return;
}
debugger;
datasourceSrv.get(datasource).then(function(ds) {
$scope.datasource = ds;
});
};
$scope.changeDatasource = function(datasource) {
@ -90,28 +88,37 @@ function (angular, _, config) {
return $scope.dashboardViewState.fullscreen && !$scope.fullscreen;
};
$scope.get_data = function() {
if ($scope.otherPanelInFullscreenMode()) { return; }
delete $scope.panelMeta.error;
$scope.panelMeta.loading = true;
if ($scope.datasource) {
return $scope.refreshData($scope.datasource);
}
datasourceSrv.get($scope.panel.datasource).then(function(datasource) {
$scope.datasource = datasource;
return $scope.refreshData($scope.datasource);
});
};
if ($scope.refreshData) {
$scope.$on("refresh", $scope.get_data);
}
// Post init phase
$scope.fullscreen = false;
$scope.editor = { index: 1 };
// $scope.datasources = datasourceSrv.getMetricSources();
// $scope.setDatasource($scope.panel.datasource);
$scope.dashboardViewState.registerPanel($scope);
$scope.datasources = datasourceSrv.getMetricSources();
if ($scope.get_data) {
var panel_get_data = $scope.get_data;
$scope.get_data = function() {
if ($scope.otherPanelInFullscreenMode()) { return; }
delete $scope.panelMeta.error;
$scope.panelMeta.loading = true;
panel_get_data();
};
if (!$scope.skipDataOnInit) {
if (!$scope.skipDataOnInit) {
$timeout(function() {
$scope.get_data();
}
}, 30);
}
};
});

View File

@ -47,11 +47,9 @@ function (angular, app, _, config, PanelMeta) {
if ($scope.isNewPanel()) {
$scope.panel.title = "Starred Dashboards";
}
$scope.$on('refresh', $scope.get_data);
};
$scope.get_data = function() {
$scope.refreshData = function() {
var params = {
limit: $scope.panel.limit
};

View File

@ -51,10 +51,6 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
}
});
scope.$on('refresh', function() {
scope.get_data();
});
// Receive render events
scope.$on('render',function(event, renderData) {
data = renderData || data;

View File

@ -23,7 +23,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
};
});
module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv, datasourceSrv) {
module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv) {
$scope.panelMeta = new PanelMeta({
panelName: 'Graph',
@ -169,7 +169,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
};
$scope.get_data = function() {
$scope.refreshData = function(datasource) {
$scope.updateTimeRange();
var metricsQuery = {
@ -183,17 +183,15 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard);
return datasourceSrv.get($scope.panel.datasource).then(function(ds) {
return ds.query(metricsQuery)
.then($scope.dataHandler)
.then(null, function(err) {
$scope.panelMeta.loading = false;
$scope.panelMeta.error = err.message || "Timeseries data request error";
$scope.inspector.error = err;
$scope.seriesList = [];
$scope.render([]);
});
});
return datasource.query(metricsQuery)
.then($scope.dataHandler)
.then(null, function(err) {
$scope.panelMeta.loading = false;
$scope.panelMeta.error = err.message || "Timeseries data request error";
$scope.inspector.error = err;
$scope.seriesList = [];
$scope.render([]);
});
};
$scope.dataHandler = function(results) {
@ -230,8 +228,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
var series = new TimeSeries({
datapoints: datapoints,
alias: alias,
color: color,
alias: alias,
color: color,
});
if (datapoints && datapoints.length > 0) {

View File

@ -74,7 +74,6 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) {
$scope.init = function() {
panelSrv.init($scope);
$scope.$on('refresh', $scope.get_data);
};
$scope.updateTimeRange = function () {
@ -84,7 +83,7 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) {
$scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval);
};
$scope.get_data = function() {
$scope.refreshData = function(datasource) {
$scope.updateTimeRange();
var metricsQuery = {
@ -95,7 +94,7 @@ function (angular, app, _, TimeSeries, kbn, PanelMeta) {
cacheTimeout: $scope.panel.cacheTimeout
};
return $scope.datasource.query(metricsQuery)
return datasource.query(metricsQuery)
.then($scope.dataHandler)
.then(null, function(err) {
console.log("err");

View File

@ -43,7 +43,11 @@ function (angular, app, _, require, PanelMeta) {
$scope.init = function() {
panelSrv.init($scope);
$scope.ready = false;
$scope.$on('refresh', $scope.render);
$scope.render();
};
$scope.refreshData = function() {
$scope.panelMeta.loading = false;
$scope.render();
};

View File

@ -1,4 +1,4 @@
<div ng-include src="datasource.editorSrc"></div>
<div ng-include src="datasource.meta.partials.query"></div>
<div class="editor-row" style="margin-top: 30px">

View File

@ -1,10 +0,0 @@
<div>
<div class="row-fluid">
<div class="span4">
<label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
</div>
<div class="span2" ng-show="panel.mode == 'text'">
<label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
</div>
</div>
</div>

View File

@ -1,3 +0,0 @@
<div ng-controller='CustomPanelCtrl'>
<h2>Custom panel</h2>
</div>

View File

@ -1,27 +0,0 @@
define([
'angular',
'app',
'lodash',
'components/panelmeta',
],
function (angular, app, _, PanelMeta) {
'use strict';
var module = angular.module('grafana.panels.custom', []);
app.useModule(module);
module.controller('CustomPanelCtrl', function($scope, panelSrv) {
$scope.panelMeta = new PanelMeta({
description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
});
// set and populate defaults
var _d = {
};
_.defaults($scope.panel, _d);
panelSrv.init($scope);
});
});

View File

@ -1,55 +0,0 @@
define([
'angular',
'lodash',
'kbn',
'moment'
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.factory('CustomDatasource', function($q) {
// the datasource object passed to constructor
// is the same defined in config.js
function CustomDatasource(datasource) {
this.name = datasource.name;
this.supportMetrics = true;
this.url = datasource.url;
}
CustomDatasource.prototype.query = function(options) {
// get from & to in seconds
var from = kbn.parseDate(options.range.from).getTime();
var to = kbn.parseDate(options.range.to).getTime();
var series = [];
var stepInSeconds = (to - from) / options.maxDataPoints;
for (var i = 0; i < 3; i++) {
var walker = Math.random() * 100;
var time = from;
var timeSeries = {
target: "Series " + i,
datapoints: []
};
for (var j = 0; j < options.maxDataPoints; j++) {
timeSeries.datapoints[j] = [walker, time];
walker += Math.random() - 0.5;
time += stepInSeconds;
}
series.push(timeSeries);
}
return $q.when({data: series });
};
return CustomDatasource;
});
});

View File

@ -17,18 +17,12 @@ function (angular, _, $, config, kbn, moment) {
module.factory('GraphiteDatasource', function($q, $http, templateSrv) {
function GraphiteDatasource(datasource) {
this.type = 'graphite';
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';
this.supportAnnotations = true;
this.supportMetrics = true;
this.editorSrc = 'app/features/graphite/partials/query.editor.html';
this.annotationEditorSrc = 'app/features/graphite/partials/annotations.editor.html';
}
GraphiteDatasource.prototype.query = function(options) {

View File

@ -1,20 +0,0 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.services');
module.factory('MyDataSource', function() {
function MyDataSource(datasource) {
this.type = 'my_ds';
this.datasource = datasource;
}
return MyDataSource;
});
});

View File

@ -1,7 +1,18 @@
{
"type": "datasource",
"name": "My Data source",
"keyName": "graphite"
"serviceName": "MyDataSource",
"editPartial": "./partials/edit.html",
"pluginType": "datasource",
"description": "Graphite",
"type": "graphite",
"serviceName": "GraphiteDatasource",
"module": "plugins/datasources/graphite/datasource",
"partials": {
"config": "app/plugins/datasources/graphite/partials/config.html",
"query": "app/plugins/datasources/graphite/partials/query.editor.html",
"annotations": "app/plugins/datasources/graphite/partials/query.editor.html"
},
"metrics": true,
"annotations": true
}

View File

@ -15,23 +15,14 @@ function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
module.factory('InfluxDatasource_08', function($q, $http, templateSrv) {
function InfluxDatasource(datasource) {
this.type = 'influxdb_08';
this.urls = _.map(datasource.url.split(','), function(url) {
return url.trim();
});
this.username = datasource.username;
this.password = datasource.password;
this.name = datasource.name;
this.basicAuth = datasource.basicAuth;
this.grafanaDB = datasource.grafanaDB;
this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp;
this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl;
this.supportAnnotations = true;
this.supportMetrics = true;
this.editorSrc = 'app/features/influxdb_08/partials/query.editor.html';
this.annotationEditorSrc = 'app/features/influxdb_08/partials/annotations.editor.html';
}
InfluxDatasource.prototype.query = function(options) {

View File

@ -0,0 +1,18 @@
{
"pluginType": "datasource",
"description": "InfluxDB 0.8.x",
"type": "influxdb_08",
"serviceName": "InfluxDatasource08",
"module": "plugins/datasources/influxdb_08/datasource",
"partials": {
"config": "app/plugins/datasources/influxdb_08/partials/config.html",
"query": "app/plugins/datasources/influxdb_08/partials/query.editor.html",
"annotations": "app/plugins/datasources/influxdb_08/partials/query.editor.html"
},
"metrics": true,
"annotations": true
}

View File

@ -7,37 +7,27 @@ function (angular, _, config) {
'use strict';
var module = angular.module('grafana.services');
var typeMap = {
'graphite': 'GraphiteDatasource',
'influxdb': 'InfluxDatasource',
'influxdb_08': 'InfluxDatasource_08',
'elasticsearch': 'ElasticDatasource',
'opentsdb': 'OpenTSDBDatasource',
'grafana': 'GrafanaDatasource',
};
var plugins = {
datasources: {
'graphite': {
'serviceName': 'GraphiteDatasource',
'module': 'features/graphite/datasource'
}
}
};
module.service('datasourceSrv', function($q, $injector, $rootScope) {
var self = this;
this.datasources = {};
this.metricSources = [];
this.annotationSources = [];
this.grafanaDB = new ($injector.get("GrafanaDatasource"));
this.init = function(dsSettingList) {
config.datasources = dsSettingList;
};
this.datasourceFactory = function(ds) {
var type = typeMap[ds.type] || ds.type;
var Datasource = $injector.get(type);
return new Datasource(ds);
_.each(config.datasources, function(value, key) {
if (value.meta && value.meta.metrics) {
self.metricSources.push({ value: key, name: key });
}
});
if (!config.defaultDatasource) {
$rootScope.appEvent('alert-error', ["No default data source found", ""]);
}
};
this.get = function(name) {
@ -53,18 +43,16 @@ function (angular, _, config) {
};
this.loadDatasource = function(name) {
var datasourceConfig = config.datasources[name];
var pluginDef = plugins.datasources[datasourceConfig.type];
if (!pluginDef) {
throw { message: "No plugin definition for data source: " + name };
}
var dsConfig = config.datasources[name];
var deferred = $q.defer();
var pluginDef = dsConfig.meta;
$rootScope.require([pluginDef.module], function() {
var AngularService = $injector.get(pluginDef.serviceName);
var instance = new AngularService(datasourceConfig);
var instance = new AngularService(dsConfig, pluginDef);
instance.meta = pluginDef;
instance.name = name;
self.datasources[name] = instance;
deferred.resolve(instance);
});

View File

@ -24,7 +24,7 @@ define([
]
}));
ctx.scope.render = sinon.spy();
ctx.scope.get_data();
ctx.scope.refreshData(ctx.datasource);
ctx.scope.$digest();
});
@ -36,7 +36,7 @@ define([
describe('get_data failure following success', function() {
beforeEach(function() {
ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
ctx.scope.get_data();
ctx.scope.refreshData(ctx.datasource);
ctx.scope.$digest();
});

View File

@ -14,7 +14,13 @@ define([
this.templateSrv = new TemplateSrvStub();
this.datasourceSrv = {
getMetricSources: function() {},
get: function() { return self.datasource; }
get: function() {
return {
then: function(callback) {
callback(self.datasource);
}
};
}
};
this.providePhase = function(mocks) {