Commit a82aa820 by Torkel Ödegaard

Merge branch 'master' into invite

parents 2c7e8077 0a59d6ab
......@@ -8,11 +8,13 @@ host = "127.0.0.1"
port = 389
# Set to true if ldap server supports TLS
use_ssl = false
# set to true if you want to skip ssl cert validation
ssl_skip_verify = false
# Search user bind dn
bind_dn = "cn=admin,dc=grafana,dc=org"
# Search user bind password
bind_password = "grafana"
bind_password = 'grafana'
# Search filter, for example "(cn=%s)" or "(sAMAccountName=%s)"
search_filter = "(cn=%s)"
......@@ -34,7 +36,7 @@ org_role = "Admin"
# The Grafana organization database id, optional, if left out the default org (id 1) will be used
# org_id = 1
[[server.ldap_group_to_org_role_mappings]]
[[server.group_mappings]]
group_dn = "cn=users,dc=grafana,dc=org"
org_role = "Editor"
......
......@@ -27,6 +27,8 @@ host = "127.0.0.1"
port = 389
# Set to true if ldap server supports TLS
use_ssl = false
# set to true if you want to skip ssl cert validation
ssl_skip_verify = false
# Search user bind dn
bind_dn = "cn=admin,dc=grafana,dc=org"
......
......@@ -8,42 +8,41 @@ page_keywords: grafana, singlestat, panel, documentation
![](/img/v1/singlestat_panel2.png)
The singlestat panel allows you to show the one main summery stat of a single series (like max, min, avg, sum). It also
provides thresholds to color that singlestat metric or the panel background.
The singlestat Panel allows you to show the one main summary stat of a single series (like max, min, avg, sum). It also provides thresholds to color the stat or the Panel background.
### Big Value Configuration
### Singlestat Panel Configuration
The big value configuration allows you to both customize the look of your singlestat metric, as well as add additional labels to contexualize the metric.
The singlestat panel has a normal query editor to allow you define your exact metric queries like many other Panels. Through the Options tab, you can access the Singlestat-specific functionality.
<img class="no-shadow" src="/img/v1/Singlestat-BaseSettings.png">
1. `Big Value`: Big Value refers to the collection of values displayed in the singlestat panel.
2. `Prefixes`: The Prefix fields let you define a custom label and font-size (as a %) to appear *before* the singlestat metric.
3. `Values`: The Value fields let you set the (min, max, average, current, total) and font-size (as a %) of the singlestat metric.
4. `Potsfixes`: The Postfix fields let you define a custom label and font-size (as a %) to appear *after* the singlestat metric.
5. `Units`: Units are appended to the the singlestat metric within the panel, and will respect the color and threshold settings for the Value.
6. `Decimals`: The Decimal field allows you to override automatic decimal precision, inceasing the digits displayed for your singlestat metric.
1. `Big Value`: Big Value refers to how we display the main stat for the Singlestat Panel. This is always a single value that is displayed in the Panel in between two strings, `Prefix` and `Suffix`. The single number is calculated by choosing a function (min,max,average,current,total) of your metric query. This functions reduces your query into a single numeric value.
2. `Font Size`: You can use this section
3. `Values`: The Value fields let you set the function (min, max, average, current, total) that your entire query is reduced into a single value with. You can also set the font size of theand font-size (as a %) of the metric query that the Panel is configured with. This reduces the entire query into a single summary value that is displayed.
4. `Postfixes`: The Postfix fields let you define a custom label and font-size (as a %) to appear *after* the value
5. `Units`: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value.
6. `Decimals`: The Decimal field allows you to override the automatic decimal precision, and set it explicitely.
### Coloring
The coloring options of the singlestat config allow you to dynamically change the colors based on the displayed data.
The coloring options of the Singlestat Panel config allow you to dynamically change the colors based on the Singlestat value.
<img class="no-shadow" src="/img/v1/Singlestat-Coloring.png">
1. `Background`: The Background checkbox applies the configured thresholds and colors to the entirity of the singlestat panel background.
2. `Value`: The Value checkbox applies the configured thresholds and colors to the value within the singlestat panel.
3. `Thresholds`: Thresholds allow you to change the background and value colors dyanmically within the panel. The threshold field accepts **3 comma-separated** values, corresponding to the three colors directly to the right.
4. `Colors`: The color picker allows you to select a color and opacity
1. `Background`: This checkbox applies the configured thresholds and colors to the entirity of the Singlestat Panel background.
2. `Value`: This checkbox applies the configured thresholds and colors to the summary stat.
3. `Thresholds`: Change the background and value colors dyanmically within the panel, depending on the Singlestat value. The threshold field accepts **3 comma-separated** values, corresponding to the three colors directly to the right.
4. `Colors`: Select a color and opacity
5. `Invert order`: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/v1/ryg.png">).
### Spark Lines
Spark lines are a great way of seeing the historical data associated with a single stat value, providing valuable context at a glance. Spark lines act differently than traditional graph panels and do not include x or y axis, coordinates, a legend, or ability to interact with the graph.
Sparklines are a great way of seeing the historical data related to the summary stat, providing valuable context at a glance. Sparklines act differently than traditional graph panels and do not include x or y axis, coordinates, a legend, or ability to interact with the graph.
<img class="no-shadow" src="/img/v1/Singlestat-Sparklines.png">
1. `Show`: The show checkbox will toggle whether the spark line is shown in the panel. When unselected, only the value will appear.
2. `Background`: Check if you want the sparklines to take up the full panel width or uncheck if they should only be at the bottom.
1. `Show`: The show checkbox will toggle whether the spark line is shown in the Panel. When unselected, only the Singlestat value will appear.
2. `Background`: Check if you want the sparklines to take up the full panel width, or uncheck if they should be below the main Singlestat value.
3. `Line Color`: This color selection applies to the color of the sparkline itself.
4. `Fill Color`: This color selection applies to the area below the sparkline.
......@@ -51,7 +50,7 @@ Spark lines are a great way of seeing the historical data associated with a sing
### Value to text mapping
Value to text mapping allows you to translate values into explcit text. The text will respect all styling, thresholds and customization defined for the value.
Value to text mapping allows you to translate the value of the summary stat into explicit text. The text will respect all styling, thresholds and customization defined for the value. This can be useful to translate the number of the main Singlestat value into a context-specific human-readable word or message.
<img class="no-shadow" src="/img/v1/Singlestat-ValueMapping.png">
......
package login
import (
"crypto/tls"
"errors"
"fmt"
"strings"
......@@ -25,7 +26,11 @@ func (a *ldapAuther) Dial() error {
address := fmt.Sprintf("%s:%d", a.server.Host, a.server.Port)
var err error
if a.server.UseSSL {
a.conn, err = ldap.DialTLS("tcp", address, nil)
tlsCfg := &tls.Config{
InsecureSkipVerify: a.server.SkipVerifySSL,
ServerName: a.server.Host,
}
a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
} else {
a.conn, err = ldap.Dial("tcp", address)
}
......@@ -125,14 +130,17 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
return err
}
// remove or update org roles
// update or remove org roles
for _, org := range orgsQuery.Result {
match := false
for _, group := range a.server.LdapGroups {
if org.OrgId != group.OrgId {
continue
}
if ldapUser.isMemberOf(group.GroupDN) {
match = true
if org.Role != group.OrgRole {
// update role
cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: group.OrgRole}
......@@ -142,12 +150,14 @@ func (a *ldapAuther) syncOrgRoles(user *m.User, ldapUser *ldapUserInfo) error {
}
// ignore subsequent ldap group mapping matches
break
} else {
// remove role
cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
if err := bus.Dispatch(&cmd); err != nil {
return err
}
}
}
// remove role if no mappings match
if !match {
cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
if err := bus.Dispatch(&cmd); err != nil {
return err
}
}
}
......
......@@ -139,6 +139,26 @@ func TestLdapAuther(t *testing.T) {
})
})
ldapAutherScenario("given org role is updated in config", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
{GroupDN: "cn=admin", OrgId: 1, OrgRole: "Admin"},
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Viewer"},
},
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
err := ldapAuther.syncOrgRoles(&m.User{}, &ldapUserInfo{
MemberOf: []string{"cn=users"},
})
Convey("Should update org role", func() {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
})
})
ldapAutherScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
......
package login
import (
"fmt"
"github.com/BurntSushi/toml"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
......@@ -13,12 +15,13 @@ type LdapConfig struct {
}
type LdapServerConf struct {
Host string `toml:"host"`
Port int `toml:"port"`
UseSSL bool `toml:"use_ssl"`
BindDN string `toml:"bind_dn"`
BindPassword string `toml:"bind_password"`
Attr LdapAttributeMap `toml:"attributes"`
Host string `toml:"host"`
Port int `toml:"port"`
UseSSL bool `toml:"use_ssl"`
SkipVerifySSL bool `toml:"ssl_skip_verify"`
BindDN string `toml:"bind_dn"`
BindPassword string `toml:"bind_password"`
Attr LdapAttributeMap `toml:"attributes"`
SearchFilter string `toml:"search_filter"`
SearchBaseDNs []string `toml:"search_base_dns"`
......@@ -54,8 +57,17 @@ func loadLdapConfig() {
log.Fatal(3, "Failed to load ldap config file: %s", err)
}
if len(ldapCfg.Servers) == 0 {
log.Fatal(3, "ldap enabled but no ldap servers defined in config file: %s", setting.LdapConfigFile)
}
// set default org id
for _, server := range ldapCfg.Servers {
assertNotEmptyCfg(server.Host, "host")
assertNotEmptyCfg(server.BindDN, "bind_dn")
assertNotEmptyCfg(server.SearchFilter, "search_filter")
assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns")
for _, groupMap := range server.LdapGroups {
if groupMap.OrgId == 0 {
groupMap.OrgId = 1
......@@ -63,3 +75,18 @@ func loadLdapConfig() {
}
}
}
func assertNotEmptyCfg(val interface{}, propName string) {
switch v := val.(type) {
case string:
if v == "" {
log.Fatal(3, "LDAP config file is missing option: %s", propName)
}
case []string:
if len(v) == 0 {
log.Fatal(3, "LDAP config file is missing option: %s", propName)
}
default:
fmt.Println("unknown")
}
}
......@@ -11,11 +11,15 @@ function (angular, _) {
var self = this;
this.init = function(dashboard) {
if (dashboard.snapshot) { return; }
this.iteration = new Date().getTime();
this.process(dashboard);
};
this.update = function(dashboard) {
if (dashboard.snapshot) { return; }
this.iteration = this.iteration + 1;
this.process(dashboard);
};
......
......@@ -14,16 +14,33 @@ function (angular) {
$scope.clone.title = $scope.clone.title + " Copy";
};
$scope.saveClone = function() {
backendSrv.saveDashboard($scope.clone)
.then(function(result) {
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + $scope.clone.title]);
function saveDashboard(options) {
return backendSrv.saveDashboard($scope.clone, options).then(function(result) {
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + $scope.clone.title]);
$location.url('/dashboard/db/' + result.slug);
$location.url('/dashboard/db/' + result.slug);
$scope.appEvent('dashboard-saved', $scope.clone);
$scope.dismiss();
});
}
$scope.saveClone = function() {
saveDashboard({overwrite: false}).then(null, function(err) {
if (err.data && err.data.status === "name-exists") {
err.isHandled = true;
$scope.appEvent('dashboard-saved', $scope.clone);
$scope.dismiss();
});
$scope.appEvent('confirm-modal', {
title: 'Another dashboard with the same name exists',
text: "Would you still like to save this dashboard?",
yesText: "Save & Overwrite",
icon: "fa-warning",
onConfirm: function() {
saveDashboard({overwrite: true});
}
});
}
});
};
});
......
......@@ -80,6 +80,9 @@ function(angular, _) {
// remove scopedVars
panel.scopedVars = null;
// ignore span changes
panel.span = null;
// ignore panel legend sort
if (panel.legend) {
delete panel.legend.sort;
......
......@@ -114,7 +114,7 @@ function (angular, _) {
title: linkDef.title,
icon: iconMap[linkDef.icon],
tooltip: linkDef.tooltip,
target: linkDef.targetBlank ? "_blank" : "",
target: linkDef.targetBlank ? "_blank" : "_self",
keepTime: linkDef.keepTime,
includeVars: linkDef.includeVars,
}]);
......
......@@ -62,7 +62,7 @@ function (angular, kbn, _) {
this.getPanelLinkAnchorInfo = function(link) {
var info = {};
if (link.type === 'absolute') {
info.target = link.targetBlank ? '_blank' : '';
info.target = link.targetBlank ? '_blank' : '_self';
info.href = templateSrv.replace(link.url || '');
info.title = templateSrv.replace(link.title || '');
info.href += '?';
......@@ -70,6 +70,7 @@ function (angular, kbn, _) {
else if (link.dashUri) {
info.href = 'dashboard/' + link.dashUri + '?';
info.title = templateSrv.replace(link.title || '');
info.target = link.targetBlank ? '_blank' : '';
}
else {
info.title = templateSrv.replace(link.title || '');
......
......@@ -80,6 +80,7 @@ function (angular, _, kbn) {
if (_.isArray(variable.current.value)) {
variable.current.text = variable.current.value.join(' + ');
this.selectOptionsForCurrentValue(variable);
}
templateSrv.updateTemplateData();
......@@ -128,6 +129,18 @@ function (angular, _, kbn) {
.then(_.partial(this.validateVariableSelectionState, variable));
};
this.selectOptionsForCurrentValue = function(variable) {
for (var i = 0; i < variable.current.value.length; i++) {
var value = variable.current.value[i];
for (var y = 0; y < variable.options.length; y++) {
var option = variable.options[y];
if (option.value === value) {
option.selected = true;
}
}
}
};
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
if (!variable.options.length) { return; }
......@@ -135,15 +148,7 @@ function (angular, _, kbn) {
}
if (_.isArray(variable.current.value)) {
for (var i = 0; i < variable.current.value.length; i++) {
var value = variable.current.value[i];
for (var y = 0; y < variable.options.length; y++) {
var option = variable.options[y];
if (option.value === value) {
option.selected = true;
}
}
}
this.selectOptionsForCurrentValue(variable);
} else {
var currentOption = _.findWhere(variable.options, { text: variable.current.text });
if (currentOption) {
......
......@@ -175,7 +175,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
$scope.seriesHandler = function(seriesData, index) {
var datapoints = seriesData.datapoints;
var alias = seriesData.target;
var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index];
var colorIndex = index % $rootScope.colors.length;
var color = $scope.panel.aliasColors[alias] || $rootScope.colors[colorIndex];
var series = new TimeSeries({
datapoints: datapoints,
......
......@@ -52,23 +52,25 @@ define([
var variable = {
name: 'apps',
multi: true,
current: {text: "test", value: "test"},
options: [{text: "test", value: "test"}]
current: {text: "val1", value: "val1"},
options: [{text: "val1", value: "val1"}, {text: 'val2', value: 'val2'}]
};
beforeEach(function() {
var dashboard = { templating: { list: [variable] } };
var urlParams = {};
urlParams["var-apps"] = ["new", "other"];
urlParams["var-apps"] = ["val1", "val2"];
ctx.$location.search = sinon.stub().returns(urlParams);
ctx.service.init(dashboard);
});
it('should update current value', function() {
expect(variable.current.value.length).to.be(2);
expect(variable.current.value[0]).to.be("new");
expect(variable.current.value[1]).to.be("other");
expect(variable.current.text).to.be("new + other");
expect(variable.current.value[0]).to.be("val1");
expect(variable.current.value[1]).to.be("val2");
expect(variable.current.text).to.be("val1 + val2");
expect(variable.options[0].selected).to.be(true);
expect(variable.options[1].selected).to.be(true);
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment