Merge branch 'master' of github.com:grafana/grafana

This commit is contained in:
Torkel Ödegaard 2016-03-08 15:32:15 +01:00
commit 74ebc2d3b0
16 changed files with 275 additions and 115 deletions

View File

@ -1,6 +1,13 @@
# http://editorconfig.org
root = true
[*.go]
indent_style = tabs
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*]
indent_style = space
indent_size = 2

View File

@ -74,6 +74,39 @@ page_keywords: grafana, admin, http, api, documentation, datasource
"jsonData":null
}
## Get a single data sources by Name
`GET /api/datasources/name/:name`
**Example Request**:
GET /api/datasources/name/test_datasource HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"id":1,
"orgId":1,
"name":"test_datasource",
"type":"graphite",
"access":"proxy",
"url":"http://mydatasource.com",
"password":"",
"user":"",
"database":"",
"basicAuth":false,
"basicAuthUser":"",
"basicAuthPassword":"",
"isDefault":false,
"jsonData":null
}
## Create data source
`POST /api/datasources`

View File

@ -53,7 +53,10 @@ Request object passed to datasource.query function
}
```
Expected response from datasource.query
There are two different kind of results for datasources.
Time series and table. Time series is the most common format and is suppoert by all datasources and panels. Table format is only support by the Influxdb datasource and table panel. But we might se more of this in the future.
Time series response from datasource.query
An array of
```json
[
@ -74,6 +77,42 @@ An array of
]
```
Table response from datasource.query
An array of
```json
[
{
"columns": [
{
"text": "Time",
"type": "time",
"sort": true,
"desc": true,
},
{
"text": "mean",
},
{
"text": "sum",
}
],
"rows": [
[
1457425380000,
null,
null
],
[
1457425370000,
1002.76215352,
1002.76215352
],
],
"type": "table"
}
]
```
### Annotation Query
Request object passed to datasource.annotationsQuery function

View File

@ -174,6 +174,10 @@ func Register(r *macaron.Macaron) {
r.Get("/plugins", GetDataSourcePlugins)
}, reqOrgAdmin)
r.Group("/datasources/name/:name", func() {
r.Get("/", wrap(GetDataSourceByName))
}, reqOrgAdmin)
r.Get("/frontend/settings/", GetFrontendSettings)
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)

View File

@ -52,24 +52,9 @@ func GetDataSourceById(c *middleware.Context) Response {
}
ds := query.Result
dtos := convertModelToDtos(ds)
return Json(200, &dtos.DataSource{
Id: ds.Id,
OrgId: ds.OrgId,
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
Access: ds.Access,
Password: ds.Password,
Database: ds.Database,
User: ds.User,
BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
})
return Json(200, &dtos)
}
func DeleteDataSource(c *middleware.Context) {
@ -132,3 +117,40 @@ func GetDataSourcePlugins(c *middleware.Context) {
c.JSON(200, dsList)
}
}
// Get /api/datasources/name/:name
func GetDataSourceByName(c *middleware.Context) Response {
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrDataSourceNotFound {
return ApiError(404, "Data source not found", nil)
}
return ApiError(500, "Failed to query datasources", err)
}
ds := query.Result
dtos := convertModelToDtos(ds)
return Json(200, &dtos)
}
func convertModelToDtos(ds m.DataSource) dtos.DataSource {
return dtos.DataSource{
Id: ds.Id,
OrgId: ds.OrgId,
Name: ds.Name,
Url: ds.Url,
Type: ds.Type,
Access: ds.Access,
Password: ds.Password,
Database: ds.Database,
User: ds.User,
BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
}
}

View File

@ -26,8 +26,8 @@ func validateInput(c CommandLine, pluginFolder string) error {
return errors.New("missing path flag")
}
fileinfo, err := os.Stat(pluginDir)
if err != nil && !fileinfo.IsDir() {
fileInfo, err := os.Stat(pluginDir)
if err != nil && !fileInfo.IsDir() {
return errors.New("path is not a directory")
}
@ -49,11 +49,12 @@ func installCommand(c CommandLine) error {
log.Infof("version: %v\n", version)
}
return InstallPlugin(pluginToInstall, pluginFolder, version, c.GlobalString("repo"))
return InstallPlugin(pluginToInstall, version, c)
}
func InstallPlugin(pluginName, pluginFolder, version, repoUrl string) error {
plugin, err := s.GetPlugin(pluginName, repoUrl)
func InstallPlugin(pluginName, version string, c CommandLine) error {
plugin, err := s.GetPlugin(pluginName, c.GlobalString("repo"))
pluginFolder := c.GlobalString("path")
if err != nil {
return err
}
@ -85,7 +86,7 @@ func InstallPlugin(pluginName, pluginFolder, version, repoUrl string) error {
res, _ := s.ReadPlugin(pluginFolder, pluginName)
for _, v := range res.Dependency.Plugins {
InstallPlugin(v.Id, pluginFolder, "", repoUrl)
InstallPlugin(v.Id, version, c)
log.Infof("Installed Dependency: %v ✔\n", v.Id)
}
@ -106,12 +107,26 @@ func SelectVersion(plugin m.Plugin, version string) (m.Version, error) {
return m.Version{}, errors.New("Could not find the version your looking for")
}
func RemoveGitBuildFromname(pluginname, filename string) string {
func RemoveGitBuildFromName(pluginName, filename string) string {
r := regexp.MustCompile("^[a-zA-Z0-9_.-]*/")
return r.ReplaceAllString(filename, pluginname+"/")
return r.ReplaceAllString(filename, pluginName+"/")
}
func downloadFile(pluginName, filepath, url string) (err error) {
var retryCount = 0
func downloadFile(pluginName, filePath, url string) (err error) {
defer func() {
if r := recover(); r != nil {
retryCount++
if retryCount == 1 {
log.Debug("\nFailed downloading. Will retry once.\n")
downloadFile(pluginName, filePath, url)
} else {
panic(r)
}
}
}()
resp, err := http.Get(url)
if err != nil {
return err
@ -122,24 +137,18 @@ func downloadFile(pluginName, filepath, url string) (err error) {
if err != nil {
return err
}
log.Infof("Got statuscode %s from %s\n", resp.Status, url)
if resp.StatusCode == 302 || resp.StatusCode == 301 {
str, _ := ioutil.ReadAll(resp.Body)
log.Info("body %s\n\n", string(str))
}
r, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength)
if err != nil {
return err
}
for _, zf := range r.File {
newfile := path.Join(filepath, RemoveGitBuildFromname(pluginName, zf.Name))
newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name))
if zf.FileInfo().IsDir() {
os.Mkdir(newfile, 0777)
os.Mkdir(newFile, 0777)
} else {
dst, err := os.Create(newfile)
dst, err := os.Create(newFile)
if err != nil {
log.Errorf("%v", err)
}

View File

@ -19,7 +19,7 @@ func TestFoldernameReplacement(t *testing.T) {
Convey("should be replaced with plugin name", func() {
for k, v := range paths {
So(RemoveGitBuildFromname(pluginName, k), ShouldEqual, v)
So(RemoveGitBuildFromName(pluginName, k), ShouldEqual, v)
}
})
})
@ -32,7 +32,7 @@ func TestFoldernameReplacement(t *testing.T) {
Convey("should be replaced with plugin name", func() {
for k, v := range paths {
So(RemoveGitBuildFromname(pluginName, k), ShouldEqual, v)
So(RemoveGitBuildFromName(pluginName, k), ShouldEqual, v)
}
})
})

View File

@ -54,7 +54,7 @@ func upgradeAllCommand(c CommandLine) error {
log.Infof("Upgrading %v \n", p.Id)
s.RemoveInstalledPlugin(pluginDir, p.Id)
InstallPlugin(p.Id, pluginDir, "", c.GlobalString("repo"))
InstallPlugin(p.Id, "", c)
}
return nil

View File

@ -24,7 +24,7 @@ func upgradeCommand(c CommandLine) error {
if localPlugin.Id == v.Id {
if ShouldUpgrade(localPlugin.Info.Version, v) {
s.RemoveInstalledPlugin(pluginDir, pluginName)
return InstallPlugin(localPlugin.Id, pluginDir, "", c.GlobalString("repo"))
return InstallPlugin(localPlugin.Id, "", c)
}
}
}

View File

@ -27,29 +27,71 @@ function (angular, _, kbn) {
var queryParams = $location.search();
var promises = [];
// use promises to delay processing variables that
// depend on other variables.
this.variableLock = {};
_.forEach(this.variables, function(variable) {
self.variableLock[variable.name] = $q.defer();
});
for (var i = 0; i < this.variables.length; i++) {
var variable = this.variables[i];
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
promises.push(this.setVariableFromUrl(variable, urlValue));
}
else if (variable.refresh) {
promises.push(this.updateOptions(variable));
}
else if (variable.type === 'interval') {
this.updateAutoInterval(variable);
}
promises.push(this.processVariable(variable, queryParams));
}
return $q.all(promises);
};
this.processVariable = function(variable, queryParams) {
var dependencies = [];
var lock = self.variableLock[variable.name];
// determine our dependencies.
if (variable.type === "query") {
_.forEach(this.variables, function(v) {
if (templateSrv.containsVariable(variable.query, v.name)) {
dependencies.push(self.variableLock[v.name].promise);
}
});
}
return $q.all(dependencies).then(function() {
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
return self.setVariableFromUrl(variable, urlValue).then(lock.resolve);
}
else if (variable.refresh) {
return self.updateOptions(variable).then(function() {
if (_.isEmpty(variable.current) && variable.options.length) {
console.log("setting current for %s", variable.name);
self.setVariableValue(variable, variable.options[0]);
}
lock.resolve();
});
}
else if (variable.type === 'interval') {
self.updateAutoInterval(variable);
lock.resolve();
} else {
lock.resolve();
}
});
};
this.setVariableFromUrl = function(variable, urlValue) {
var promise = $q.when(true);
if (variable.refresh) {
promise = this.updateOptions(variable);
}
return promise.then(function() {
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
this.updateAutoInterval(variable);
return this.setVariableValue(variable, option);
self.updateAutoInterval(variable);
return self.setVariableValue(variable, option, true);
});
};
this.updateAutoInterval = function(variable) {
@ -64,7 +106,7 @@ function (angular, _, kbn) {
templateSrv.setGrafanaVariable('$__auto_interval', interval);
};
this.setVariableValue = function(variable, option) {
this.setVariableValue = function(variable, option, initPhase) {
variable.current = angular.copy(option);
if (_.isArray(variable.current.value)) {
@ -72,8 +114,14 @@ function (angular, _, kbn) {
}
self.selectOptionsForCurrentValue(variable);
templateSrv.updateTemplateData();
// on first load, variable loading is ordered to ensure
// that parents are updated before children.
if (initPhase) {
return $q.when();
}
return self.updateOptionsInChildVariables(variable);
};
@ -145,7 +193,7 @@ function (angular, _, kbn) {
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
if (!variable.options.length) { return; }
return self.setVariableValue(variable, variable.options[0]);
return self.setVariableValue(variable, variable.options[0], true);
}
if (_.isArray(variable.current.value)) {
@ -153,7 +201,7 @@ function (angular, _, kbn) {
} else {
var currentOption = _.findWhere(variable.options, { text: variable.current.text });
if (currentOption) {
return self.setVariableValue(variable, currentOption);
return self.setVariableValue(variable, currentOption, true);
} else {
if (!variable.options.length) { return; }
return self.setVariableValue(variable, variable.options[0]);

View File

@ -1,74 +1,70 @@
<section class="grafana-metric-options">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item tight-form-item-icon">
<section class="grafana-metric-options gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">
<i class="fa fa-wrench"></i>
</li>
<li class="tight-form-item">
Cache timeout
</li>
<li>
</span>
<span class="gf-form-label width-8">Cache timeout</span>
<input type="text"
class="input-mini tight-form-input"
class="gf-form-input"
ng-model="ctrl.panelCtrl.panel.cacheTimeout"
bs-tooltip="'Graphite parameter to override memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
placeholder="60"></input>
</li>
<li class="tight-form-item">
Max data points
</li>
<li>
placeholder="60">
</input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Max data points</span>
<input type="text"
class="input-mini tight-form-input"
class="gf-form-input"
ng-model="ctrl.panelCtrl.panel.maxDataPoints"
bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
data-placement="right"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
spellcheck='false'
placeholder="auto"></input>
</li>
</ul>
<div class="clearfix"></div>
placeholder="auto">
</input>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item tight-form-item-icon">
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-12">
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
shorter legend names
</a>
</li>
<li class="tight-form-item">
</span>
<span class="gf-form-label width-12">
<i class="fa fa-info-circle"></i>
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
series as parameters
</a>
</li>
<li class="tight-form-item">
</span>
<span class="gf-form-label width-7">
<i class="fa fa-info-circle"></i>
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking
</a>
</li>
<li class="tight-form-item">
</span>
<span class="gf-form-label width-8">
<i class="fa fa-info-circle"></i>
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
templating
</a>
</li>
<li class="tight-form-item">
</span>
<span class="gf-form-label width-10">
<i class="fa fa-info-circle"></i>
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
max data points
</a>
</li>
</ul>
<div class="clearfix"></div>
</span>
</div>
</div>
</section>
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="pull-left">
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
<h5>Shorter legend names</h5>

View File

@ -2,7 +2,7 @@
import './graph';
import './legend';
import './seriesOverridesCtrl';
import './series_overrides_ctrl';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';

View File

@ -1,4 +1,4 @@
<div class="editor-row">
<div class="editor-row gf-form-group">
<div class="section">
<h5>Chart Options</h5>
<editor-opt-bool text="Bars" model="ctrl.panel.bars" change="ctrl.render()"></editor-opt-bool>

View File

@ -1,6 +1,6 @@
define([
'./helpers',
'app/plugins/panel/graph/seriesOverridesCtrl'
'app/plugins/panel/graph/series_overrides_ctrl'
], function(helpers) {
'use strict';

View File

@ -34,12 +34,13 @@ define([
options: [{text: "test", value: "test"}]
};
beforeEach(function() {
beforeEach(function(done) {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = "new";
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard);
ctx.service.init(dashboard).then(function() { done(); });
ctx.$rootScope.$digest();
});
it('should update current value', function() {
@ -56,12 +57,13 @@ define([
options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}, {text: 'val3', value: 'val3', selected: true}]
};
beforeEach(function() {
beforeEach(function(done) {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = ["val2", "val1"];
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard);
ctx.service.init(dashboard).then(function() { done(); });
ctx.$rootScope.$digest();
});
it('should update current value', function() {