mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 03:41:50 +08:00
Merge branch 'master' into apps
Conflicts: pkg/services/sqlstore/migrations/migrations.go
This commit is contained in:
@ -7,12 +7,16 @@
|
||||
|
||||
### Enhancements ###
|
||||
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
|
||||
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
|
||||
|
||||
# 2.6.1 (unrelased, 2.6.x branch)
|
||||
|
||||
### New Features
|
||||
* **Elasticsearch**: Support for derivative unit option, closes [#3512](https://github.com/grafana/grafana/issues/3512)
|
||||
|
||||
### Bug fixes
|
||||
* **Graph Panel**: Fixed typehead when adding series style override, closes [#3554](https://github.com/grafana/grafana/issues/3554)
|
||||
|
||||
# 2.6.0 (2015-12-14)
|
||||
|
||||
### New Features
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright 2014-2015 Torkel Ödegaard, Raintank Inc.
|
||||
Copyright 2014-2016 Torkel Ödegaard, Raintank Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you
|
||||
may not use this file except in compliance with the License. You may
|
||||
|
@ -63,9 +63,15 @@ name = grafana
|
||||
user = root
|
||||
password =
|
||||
|
||||
# For "postgres" only, either "disable", "require" or "verify-full"
|
||||
# For "postgres", use either "disable", "require" or "verify-full"
|
||||
# For "mysql", use either "true", "false", or "skip-verify".
|
||||
ssl_mode = disable
|
||||
|
||||
ca_cert_path =
|
||||
client_key_path =
|
||||
client_cert_path =
|
||||
server_cert_name =
|
||||
|
||||
# For "sqlite3" only, path relative to data_path setting
|
||||
path = grafana.db
|
||||
|
||||
@ -79,7 +85,9 @@ provider = file
|
||||
# file: session dir path, is relative to grafana data_path
|
||||
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
|
||||
# postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
|
||||
# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
|
||||
# mysql: go-sql-driver/mysql dsn config string, examples:
|
||||
# `user:password@tcp(127.0.0.1:3306)/database_name`
|
||||
# `user:password@unix(/var/run/mysqld/mysqld.sock)/database_name`
|
||||
# memcache: 127.0.0.1:11211
|
||||
|
||||
|
||||
|
@ -77,6 +77,11 @@ The Elasticsearch datasource supports two types of queries you can use to fill t
|
||||
{"find": "fields", "type": "string"}
|
||||
```
|
||||
|
||||
### Fields filtered by type, with filter
|
||||
```json
|
||||
{"find": "fields", "type": "string", "query": <lucene query>}
|
||||
```
|
||||
|
||||
### Multi format / All format
|
||||
Use lucene format.
|
||||
|
||||
|
@ -156,7 +156,24 @@ The database user's password (not applicable for `sqlite3`).
|
||||
|
||||
### ssl_mode
|
||||
|
||||
For `postgres` only, either `disable`, `require` or `verify-full`.
|
||||
For Postgres, use either `disable`, `require` or `verify-full`.
|
||||
For MySQL, use either `true`, `false`, or `skip-verify`.
|
||||
|
||||
### ca_cert_path
|
||||
|
||||
(MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
|
||||
|
||||
### client_key_path
|
||||
|
||||
(MySQL only) The path to the client key. Only if server requires client authentication.
|
||||
|
||||
### client_cert_path
|
||||
|
||||
(MySQL only) The path to the client cert. Only if server requires client authentication.
|
||||
|
||||
### server_cert_name
|
||||
|
||||
(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`.
|
||||
|
||||
<hr />
|
||||
|
||||
|
@ -79,6 +79,9 @@ func scan(pluginDir string) error {
|
||||
}
|
||||
|
||||
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
|
||||
if pluginDir != "data/plugins" {
|
||||
log.Warn("Could not scan dir \"%v\" error: %s", pluginDir, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addDashboardSnapshotMigrations(mg)
|
||||
addQuotaMigration(mg)
|
||||
addAppPluginMigration(mg)
|
||||
addSessionMigration(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
16
pkg/services/sqlstore/migrations/session_mig.go
Normal file
16
pkg/services/sqlstore/migrations/session_mig.go
Normal file
@ -0,0 +1,16 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addSessionMigration(mg *Migrator) {
|
||||
var sessionV1 = Table{
|
||||
Name: "session",
|
||||
Columns: []*Column{
|
||||
{Name: "key", Type: DB_Char, IsPrimaryKey: true, Length: 16},
|
||||
{Name: "data", Type: DB_Blob},
|
||||
{Name: "expiry", Type: DB_Integer, Length: 255, Nullable: false},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create session table", NewAddTableMigration(sessionV1))
|
||||
}
|
@ -14,12 +14,21 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/go-xorm/xorm"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type MySQLConfig struct {
|
||||
SslMode string
|
||||
CaCertPath string
|
||||
ClientKeyPath string
|
||||
ClientCertPath string
|
||||
ServerCertName string
|
||||
}
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
dialect migrator.Dialect
|
||||
@ -30,6 +39,8 @@ var (
|
||||
Type, Host, Name, User, Pwd, Path, SslMode string
|
||||
}
|
||||
|
||||
mysqlConfig MySQLConfig
|
||||
|
||||
UseSQLite3 bool
|
||||
)
|
||||
|
||||
@ -113,8 +124,22 @@ func getEngine() (*xorm.Engine, error) {
|
||||
cnnstr := ""
|
||||
switch DbCfg.Type {
|
||||
case "mysql":
|
||||
cnnstr = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8",
|
||||
DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name)
|
||||
protocol := "tcp"
|
||||
if strings.HasPrefix(DbCfg.Host, "/") {
|
||||
protocol = "unix"
|
||||
}
|
||||
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8",
|
||||
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
|
||||
|
||||
if mysqlConfig.SslMode == "true" || mysqlConfig.SslMode == "skip-verify" {
|
||||
tlsCert, err := makeCert("custom", mysqlConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mysql.RegisterTLSConfig("custom", tlsCert)
|
||||
cnnstr += "&tls=custom"
|
||||
}
|
||||
case "postgres":
|
||||
var host, port = "127.0.0.1", "5432"
|
||||
fields := strings.Split(DbCfg.Host, ":")
|
||||
@ -156,4 +181,12 @@ func LoadConfig() {
|
||||
}
|
||||
DbCfg.SslMode = sec.Key("ssl_mode").String()
|
||||
DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
|
||||
|
||||
if DbCfg.Type == "mysql" {
|
||||
mysqlConfig.SslMode = DbCfg.SslMode
|
||||
mysqlConfig.CaCertPath = sec.Key("ca_cert_path").String()
|
||||
mysqlConfig.ClientKeyPath = sec.Key("client_key_path").String()
|
||||
mysqlConfig.ClientCertPath = sec.Key("client_cert_path").String()
|
||||
mysqlConfig.ServerCertName = sec.Key("server_cert_name").String()
|
||||
}
|
||||
}
|
||||
|
41
pkg/services/sqlstore/tls_mysql.go
Normal file
41
pkg/services/sqlstore/tls_mysql.go
Normal file
@ -0,0 +1,41 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func makeCert(tlsPoolName string, config MySQLConfig) (*tls.Config, error) {
|
||||
rootCertPool := x509.NewCertPool()
|
||||
pem, err := ioutil.ReadFile(config.CaCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read DB CA Cert path: %v", config.CaCertPath)
|
||||
}
|
||||
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
||||
return nil, err
|
||||
}
|
||||
clientCert := make([]tls.Certificate, 0, 1)
|
||||
if config.ClientCertPath != "" && config.ClientKeyPath != "" {
|
||||
|
||||
certs, err := tls.LoadX509KeyPair(config.ClientCertPath, config.ClientKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientCert = append(clientCert, certs)
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: rootCertPool,
|
||||
Certificates: clientCert,
|
||||
}
|
||||
tlsConfig.ServerName = config.ServerCertName
|
||||
if config.SslMode == "skip-verify" {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
// Return more meaningful error before it is too late
|
||||
if config.ServerCertName == "" && !tlsConfig.InsecureSkipVerify {
|
||||
return nil, fmt.Errorf("server_cert_name is missing. Consider using ssl_mode = skip-verify.")
|
||||
}
|
||||
return tlsConfig, nil
|
||||
}
|
@ -74,11 +74,10 @@ function (_, $, coreModule) {
|
||||
updater: function (value) {
|
||||
var result = {};
|
||||
_.each($scope.menuItems, function(menuItem) {
|
||||
result.$item = menuItem;
|
||||
|
||||
_.each(menuItem.submenu, function(submenuItem) {
|
||||
if (value === (menuItem.text + ' ' + submenuItem.text)) {
|
||||
result.$subItem = submenuItem;
|
||||
result.$item = menuItem;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,44 +0,0 @@
|
||||
define([
|
||||
"angular",
|
||||
"lodash",
|
||||
"moment",
|
||||
],function (angular, _, moment) {
|
||||
'use strict';
|
||||
|
||||
angular.
|
||||
module("grafana.directives").
|
||||
directive('inputDatetime', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $elem, attrs, ngModel) {
|
||||
var format = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
var fromUser = function (text) {
|
||||
if (text.indexOf('now') !== -1) {
|
||||
return text;
|
||||
}
|
||||
var parsed;
|
||||
if ($scope.ctrl.isUtc) {
|
||||
parsed = moment.utc(text, format);
|
||||
} else {
|
||||
parsed = moment(text, format);
|
||||
}
|
||||
|
||||
return parsed.isValid() ? parsed : undefined;
|
||||
};
|
||||
|
||||
var toUser = function (currentValue) {
|
||||
if (moment.isMoment(currentValue)) {
|
||||
return currentValue.format(format);
|
||||
} else {
|
||||
return currentValue;
|
||||
}
|
||||
};
|
||||
|
||||
ngModel.$parsers.push(fromUser);
|
||||
ngModel.$formatters.push(toUser);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
41
public/app/features/dashboard/timepicker/input_date.ts
Normal file
41
public/app/features/dashboard/timepicker/input_date.ts
Normal file
@ -0,0 +1,41 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
|
||||
export function inputDateDirective() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $elem, attrs, ngModel) {
|
||||
var format = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
var fromUser = function (text) {
|
||||
if (text.indexOf('now') !== -1) {
|
||||
return text;
|
||||
}
|
||||
var parsed;
|
||||
if ($scope.ctrl.isUtc) {
|
||||
parsed = moment.utc(text, format);
|
||||
} else {
|
||||
parsed = moment(text, format);
|
||||
}
|
||||
|
||||
return parsed.isValid() ? parsed : undefined;
|
||||
};
|
||||
|
||||
var toUser = function (currentValue) {
|
||||
if (moment.isMoment(currentValue)) {
|
||||
return currentValue.format(format);
|
||||
} else {
|
||||
return currentValue;
|
||||
}
|
||||
};
|
||||
|
||||
ngModel.$parsers.push(fromUser);
|
||||
ngModel.$formatters.push(toUser);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
///<amd-dependency path="./input_date" name="inputDate" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
@ -9,8 +8,6 @@ import moment from 'moment';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||
|
||||
declare var inputDate: any;
|
||||
|
||||
export class TimePickerCtrl {
|
||||
|
||||
static tooltipFormat = 'MMM D, YYYY HH:mm:ss';
|
||||
@ -179,3 +176,6 @@ export function timePickerDirective() {
|
||||
|
||||
angular.module('grafana.directives').directive('gfTimePickerSettings', settingsDirective);
|
||||
angular.module('grafana.directives').directive('gfTimePicker', timePickerDirective);
|
||||
|
||||
import {inputDateDirective} from './input_date';
|
||||
angular.module("grafana.directives").directive('inputDatetime', inputDateDirective);
|
||||
|
@ -56,7 +56,7 @@ export class GrafanaApp {
|
||||
'ang-drag-drop',
|
||||
'grafana',
|
||||
'pasvaz.bindonce',
|
||||
'ui.bootstrap.tabs',
|
||||
'ui.bootstrap',
|
||||
'ui.bootstrap.tpls',
|
||||
];
|
||||
|
||||
|
@ -7,7 +7,7 @@ function (angular, _) {
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PrometheusQueryCtrl', function($scope) {
|
||||
module.controller('PrometheusQueryCtrl', function($scope, templateSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
var target = $scope.target;
|
||||
@ -46,9 +46,9 @@ function (angular, _) {
|
||||
|
||||
$scope.linkToPrometheus = function() {
|
||||
var range = Math.ceil(($scope.range.to.valueOf() - $scope.range.from.valueOf()) / 1000);
|
||||
var endTime = $scope.range.to.utc().format('YYYY-MM-DD HH:MM');
|
||||
var endTime = $scope.range.to.utc().format('YYYY-MM-DD HH:mm');
|
||||
var expr = {
|
||||
expr: $scope.target.expr,
|
||||
expr: templateSrv.replace($scope.target.expr, $scope.panel.scopedVars),
|
||||
range_input: range + 's',
|
||||
end_input: endTime,
|
||||
step_input: '',
|
||||
|
@ -181,6 +181,13 @@
|
||||
<li class="tight-form-item">
|
||||
<editor-checkbox text="Right side" model="panel.legend.rightSide" change="render()"></editor-checkbox>
|
||||
</li>
|
||||
<li ng-if="panel.legend.rightSide" class="tight-form-item">
|
||||
Side width
|
||||
</li>
|
||||
<li ng-if="panel.legend.rightSide" style="width: 105px">
|
||||
<input type="number" class="input-small tight-form-input" placeholder="250" bs-tooltip="'Set a min-width for the legend side table/block'" data-placement="right"
|
||||
ng-model="panel.legend.sideWidth" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -79,9 +79,9 @@ function ($) {
|
||||
// Stacked series can increase its length on each new stacked serie if null points found,
|
||||
// to speed the index search we begin always on the last found hoverIndex.
|
||||
var newhoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
|
||||
results.push({ value: value, hoverIndex: newhoverIndex});
|
||||
results.push({ value: value, hoverIndex: newhoverIndex, color: series.color, label: series.label });
|
||||
} else {
|
||||
results.push({ value: value, hoverIndex: hoverIndex});
|
||||
results.push({ value: value, hoverIndex: hoverIndex, color: series.color, label: series.label });
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +126,8 @@ function ($) {
|
||||
relativeTime = dashboard.getRelativeTime(seriesHoverInfo.time);
|
||||
absoluteTime = dashboard.formatDate(seriesHoverInfo.time);
|
||||
|
||||
seriesHoverInfo.sort(byToolTipValue);
|
||||
|
||||
for (i = 0; i < seriesHoverInfo.length; i++) {
|
||||
hoverInfo = seriesHoverInfo[i];
|
||||
|
||||
@ -138,7 +140,7 @@ function ($) {
|
||||
value = series.formatValue(hoverInfo.value);
|
||||
|
||||
seriesHtml += '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||
seriesHtml += '<i class="fa fa-minus" style="color:' + series.color +';"></i> ' + series.label + ':</div>';
|
||||
seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
|
||||
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
||||
plot.highlight(i, hoverInfo.hoverIndex);
|
||||
}
|
||||
@ -174,5 +176,9 @@ function ($) {
|
||||
});
|
||||
}
|
||||
|
||||
function byToolTipValue(a, b) {
|
||||
return parseFloat(b.value) - parseFloat(a.value);
|
||||
}
|
||||
|
||||
return GraphTooltip;
|
||||
});
|
||||
|
@ -101,6 +101,10 @@ function (angular, _, $) {
|
||||
|
||||
$container.empty();
|
||||
|
||||
// Set min-width if side style and there is a value, otherwise remove the CSS propery
|
||||
var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : "";
|
||||
$container.css("min-width", width);
|
||||
|
||||
$container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
||||
|
||||
if (panel.legend.alignAsTable) {
|
||||
|
@ -97,7 +97,8 @@ function (angular, app, _, $) {
|
||||
plotCss.bottom = '5px';
|
||||
plotCss.left = '-5px';
|
||||
plotCss.width = (width - 10) + 'px';
|
||||
plotCss.height = (height - 45) + 'px';
|
||||
var dynamicHeightMargin = height <= 100 ? 5 : (Math.round((height/100)) * 15) + 5;
|
||||
plotCss.height = (height - dynamicHeightMargin) + 'px';
|
||||
}
|
||||
else {
|
||||
plotCss.bottom = "0px";
|
||||
|
Reference in New Issue
Block a user