diff --git a/conf/ldap.toml b/conf/ldap.toml index 957fc1258ba..7deb090f9d5 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -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" diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 261d975886a..4cba472d053 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -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" diff --git a/docs/sources/reference/singlestat.md b/docs/sources/reference/singlestat.md index 4e5a38544c5..40a3551a4e0 100644 --- a/docs/sources/reference/singlestat.md +++ b/docs/sources/reference/singlestat.md @@ -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. -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. -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.
For example: Green, Orange, Red () will become Red, Orange, Green (). ### 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. -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. diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 324ba6d18a4..abc3c6a587a 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -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 } } } diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index 3d19115f878..6713d0ca3fe 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -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{ diff --git a/pkg/login/settings.go b/pkg/login/settings.go index 9b5f68a252c..0a50df33c19 100644 --- a/pkg/login/settings.go +++ b/pkg/login/settings.go @@ -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") + } +} diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index bb58e2dbc06..8e3c24f7202 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -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); }; diff --git a/public/app/features/dashboard/saveDashboardAsCtrl.js b/public/app/features/dashboard/saveDashboardAsCtrl.js index 16dc291e9fa..ec8327755f8 100644 --- a/public/app/features/dashboard/saveDashboardAsCtrl.js +++ b/public/app/features/dashboard/saveDashboardAsCtrl.js @@ -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}); + } + }); + } + }); }; }); diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js index 2b220bda9f8..758c06b1975 100644 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ b/public/app/features/dashboard/unsavedChangesSrv.js @@ -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; diff --git a/public/app/features/dashlinks/module.js b/public/app/features/dashlinks/module.js index 9fc1bc4d4b5..23ce8c0ffae 100644 --- a/public/app/features/dashlinks/module.js +++ b/public/app/features/dashlinks/module.js @@ -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, }]); diff --git a/public/app/features/panellinks/linkSrv.js b/public/app/features/panellinks/linkSrv.js index c11cdb40b52..49ddcb95316 100644 --- a/public/app/features/panellinks/linkSrv.js +++ b/public/app/features/panellinks/linkSrv.js @@ -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 || ''); diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index a5767ffa312..c1d787804d7 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -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) { diff --git a/public/app/panels/graph/module.js b/public/app/panels/graph/module.js index 867f864123c..ea0a1e958ad 100644 --- a/public/app/panels/graph/module.js +++ b/public/app/panels/graph/module.js @@ -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, diff --git a/public/test/specs/templateValuesSrv-specs.js b/public/test/specs/templateValuesSrv-specs.js index 6c7a3035ff9..d0af589a068 100644 --- a/public/test/specs/templateValuesSrv-specs.js +++ b/public/test/specs/templateValuesSrv-specs.js @@ -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); }); });