mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
Merge branch 'master' into invite
This commit is contained in:
commit
a82aa8203b
@ -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
|
||||
|
||||

|
||||
|
||||
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">
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
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{
|
||||
|
@ -1,6 +1,8 @@
|
||||
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";
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
$scope.appEvent('dashboard-saved', $scope.clone);
|
||||
$scope.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.saveClone = function() {
|
||||
backendSrv.saveDashboard($scope.clone)
|
||||
.then(function(result) {
|
||||
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + $scope.clone.title]);
|
||||
saveDashboard({overwrite: false}).then(null, function(err) {
|
||||
if (err.data && err.data.status === "name-exists") {
|
||||
err.isHandled = true;
|
||||
|
||||
$location.url('/dashboard/db/' + result.slug);
|
||||
|
||||
$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);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user