From 1dd65f7a39f0f25985551bc4ebff671f70493a5a Mon Sep 17 00:00:00 2001 From: Aman Date: Wed, 9 May 2018 13:32:44 +0530 Subject: [PATCH 01/17] Add options to colorize prefix and postfix in singlestat --- .../app/plugins/panel/singlestat/editor.html | 4 ++++ public/app/plugins/panel/singlestat/module.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index 15f4e6a9efab4..3b75cf87eaeeb 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -58,6 +58,10 @@
Coloring
+
+ + +
@@ -39,7 +39,7 @@
Value
- +
@@ -59,8 +59,8 @@
Coloring
- - + +
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 0cf3a3a95a7da..ebd2628b0864c 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -198,8 +198,8 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.setValueMapping(data); } - canChangeFontSize() { - return this.panel.gauge.show; + canModifyText() { + return !this.panel.gauge.show; } setColoring(options) { From c2381f088f69637c48ee8ced84d8604e71436013 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 28 Jun 2018 12:02:49 +0200 Subject: [PATCH 03/17] Add disabled styles for checked checkbox (#12422) --- public/sass/components/_switch.scss | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index c7eb19141037e..6eb01ecc32d2b 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -64,8 +64,8 @@ } input + label::before { - font-family: "FontAwesome"; - content: "\f096"; // square-o + font-family: 'FontAwesome'; + content: '\f096'; // square-o color: $text-color-weak; transition: transform 0.4s; backface-visibility: hidden; @@ -73,11 +73,11 @@ } input + label::after { - content: "\f046"; // check-square-o + content: '\f046'; // check-square-o color: $orange; text-shadow: $text-shadow-strong; - font-family: "FontAwesome"; + font-family: 'FontAwesome'; transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; @@ -154,7 +154,8 @@ gf-form-switch[disabled] { .gf-form-switch input + label { cursor: default; pointer-events: none !important; - &::before { + &::before, + &::after { color: $text-color-faint; text-shadow: none; } From 3056d9a80eda8414539ff9c96ba2f32a219d2149 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 28 Jun 2018 12:08:32 +0200 Subject: [PATCH 04/17] support passing api token in Basic auth password (#12416) --- docs/sources/http_api/auth.md | 8 ++++++++ pkg/middleware/auth.go | 6 ++++++ pkg/middleware/middleware_test.go | 26 ++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index 166a5a4fdb922..8ff40b5ef04e4 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -44,6 +44,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk The `Authorization` header value should be `Bearer `. +The API Token can also be passed as a Basic authorization password with the special username `api_key`: + +curl example: +```bash +?curl http://api_key:eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk@localhost:3000/api/org +{"id":1,"name":"Main Org."} +``` + # Auth HTTP resources / actions ## Api Keys diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 37e79c010713c..5faee1e3fa7ad 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -9,6 +9,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type AuthOptions struct { @@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string { return key } + username, password, err := util.DecodeBasicAuthHeader(header) + if err == nil && username == "api_key" { + return password + } + return "" } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 0b50358ad7372..d82b7313585c4 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) { setting.BasicAuthEnabled = true authHeader := util.GetBasicAuthHeader("myUser", "myPass") - sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec() + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() Convey("Should init middleware context with user", func() { So(sc.context.IsSignedIn, ShouldEqual, true) @@ -128,6 +128,28 @@ func TestMiddlewareContext(t *testing.T) { }) }) + middlewareScenario("Valid api key via Basic auth", func(sc *scenarioContext) { + keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + + bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error { + query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash} + return nil + }) + + authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9") + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() + + Convey("Should return 200", func() { + So(sc.resp.Code, ShouldEqual, 200) + }) + + Convey("Should init middleware context", func() { + So(sc.context.IsSignedIn, ShouldEqual, true) + So(sc.context.OrgId, ShouldEqual, 12) + So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR) + }) + }) + middlewareScenario("UserId in session", func(sc *scenarioContext) { sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { @@ -473,7 +495,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext { return sc } -func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext { +func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext { sc.authHeader = authHeader return sc } From 443ff5deb4b010f321d41f04f676543c6c2bdb8e Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 28 Jun 2018 12:10:36 +0200 Subject: [PATCH 05/17] Karma to Jest: value_select_dropdown (#12435) * Begin Karma 2 Jest: value_select_dropdown * Add return for Promise * Remove Karma test --- .../core/directives/value_select_dropdown.ts | 4 +- .../core/specs/value_select_dropdown.jest.ts | 159 ++++++++++++++++ .../core/specs/value_select_dropdown_specs.ts | 171 ------------------ 3 files changed, 161 insertions(+), 173 deletions(-) create mode 100644 public/app/core/specs/value_select_dropdown.jest.ts delete mode 100644 public/app/core/specs/value_select_dropdown_specs.ts diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index d6c6c3af5c50a..d384904c2d85c 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl { tagValuesPromise = this.$q.when(tag.values); } - tagValuesPromise.then(values => { + return tagValuesPromise.then(values => { tag.values = values; tag.valuesText = values.join(' + '); _.each(this.options, option => { @@ -132,7 +132,7 @@ export class ValueSelectDropdownCtrl { this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length; } - selectValue(option, event, commitChange, excludeOthers) { + selectValue(option, event, commitChange?, excludeOthers?) { if (!option) { return; } diff --git a/public/app/core/specs/value_select_dropdown.jest.ts b/public/app/core/specs/value_select_dropdown.jest.ts new file mode 100644 index 0000000000000..3cc310435b7f3 --- /dev/null +++ b/public/app/core/specs/value_select_dropdown.jest.ts @@ -0,0 +1,159 @@ +import 'app/core/directives/value_select_dropdown'; +import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown'; +import q from 'q'; + +describe('SelectDropdownCtrl', () => { + let tagValuesMap: any = {}; + + ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn(); + let ctrl; + + describe('Given simple variable', () => { + beforeEach(() => { + ctrl = new ValueSelectDropdownCtrl(q); + ctrl.variable = { + current: { text: 'hej', value: 'hej' }, + getValuesForTag: key => { + return Promise.resolve(tagValuesMap[key]); + }, + }; + ctrl.init(); + }); + + it('Should init labelText and linkText', () => { + expect(ctrl.linkText).toBe('hej'); + }); + }); + + describe('Given variable with tags and dropdown is opened', () => { + beforeEach(() => { + ctrl = new ValueSelectDropdownCtrl(q); + ctrl.variable = { + current: { text: 'server-1', value: 'server-1' }, + options: [ + { text: 'server-1', value: 'server-1', selected: true }, + { text: 'server-2', value: 'server-2' }, + { text: 'server-3', value: 'server-3' }, + ], + tags: ['key1', 'key2', 'key3'], + getValuesForTag: key => { + return Promise.resolve(tagValuesMap[key]); + }, + multi: true, + }; + tagValuesMap.key1 = ['server-1', 'server-3']; + tagValuesMap.key2 = ['server-2', 'server-3']; + tagValuesMap.key3 = ['server-1', 'server-2', 'server-3']; + ctrl.init(); + ctrl.show(); + }); + + it('should init tags model', () => { + expect(ctrl.tags.length).toBe(3); + expect(ctrl.tags[0].text).toBe('key1'); + }); + + it('should init options model', () => { + expect(ctrl.options.length).toBe(3); + }); + + it('should init selected values array', () => { + expect(ctrl.selectedValues.length).toBe(1); + }); + + it('should set linkText', () => { + expect(ctrl.linkText).toBe('server-1'); + }); + + describe('after adititional value is selected', () => { + beforeEach(() => { + ctrl.selectValue(ctrl.options[2], {}); + ctrl.commitChanges(); + }); + + it('should update link text', () => { + expect(ctrl.linkText).toBe('server-1 + server-3'); + }); + }); + + describe('When tag is selected', () => { + beforeEach(async () => { + await ctrl.selectTag(ctrl.tags[0]); + ctrl.commitChanges(); + }); + + it('should select tag', () => { + expect(ctrl.selectedTags.length).toBe(1); + }); + + it('should select values', () => { + expect(ctrl.options[0].selected).toBe(true); + expect(ctrl.options[2].selected).toBe(true); + }); + + it('link text should not include tag values', () => { + expect(ctrl.linkText).toBe(''); + }); + + describe('and then dropdown is opened and closed without changes', () => { + beforeEach(() => { + ctrl.show(); + ctrl.commitChanges(); + }); + + it('should still have selected tag', () => { + expect(ctrl.selectedTags.length).toBe(1); + }); + }); + + describe('and then unselected', () => { + beforeEach(async () => { + await ctrl.selectTag(ctrl.tags[0]); + }); + + it('should deselect tag', () => { + expect(ctrl.selectedTags.length).toBe(0); + }); + }); + + describe('and then value is unselected', () => { + beforeEach(() => { + ctrl.selectValue(ctrl.options[0], {}); + }); + + it('should deselect tag', () => { + expect(ctrl.selectedTags.length).toBe(0); + }); + }); + }); + }); + + describe('Given variable with selected tags', () => { + beforeEach(() => { + ctrl = new ValueSelectDropdownCtrl(q); + ctrl.variable = { + current: { + text: 'server-1', + value: 'server-1', + tags: [{ text: 'key1', selected: true }], + }, + options: [ + { text: 'server-1', value: 'server-1' }, + { text: 'server-2', value: 'server-2' }, + { text: 'server-3', value: 'server-3' }, + ], + tags: ['key1', 'key2', 'key3'], + getValuesForTag: key => { + return Promise.resolve(tagValuesMap[key]); + }, + multi: true, + }; + ctrl.init(); + ctrl.show(); + }); + + it('should set tag as selected', () => { + expect(ctrl.tags[0].selected).toBe(true); + }); + }); +}); diff --git a/public/app/core/specs/value_select_dropdown_specs.ts b/public/app/core/specs/value_select_dropdown_specs.ts deleted file mode 100644 index 8f6408fb389c2..0000000000000 --- a/public/app/core/specs/value_select_dropdown_specs.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks, sinon } from 'test/lib/common'; -import 'app/core/directives/value_select_dropdown'; - -describe('SelectDropdownCtrl', function() { - var scope; - var ctrl; - var tagValuesMap: any = {}; - var rootScope; - var q; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach( - angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) { - rootScope = $rootScope; - q = $q; - scope = $rootScope.$new(); - ctrl = $controller('ValueSelectDropdownCtrl', { $scope: scope }); - ctrl.onUpdated = sinon.spy(); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - - describe('Given simple variable', function() { - beforeEach(function() { - ctrl.variable = { - current: { text: 'hej', value: 'hej' }, - getValuesForTag: function(key) { - return q.when(tagValuesMap[key]); - }, - }; - ctrl.init(); - }); - - it('Should init labelText and linkText', function() { - expect(ctrl.linkText).to.be('hej'); - }); - }); - - describe('Given variable with tags and dropdown is opened', function() { - beforeEach(function() { - ctrl.variable = { - current: { text: 'server-1', value: 'server-1' }, - options: [ - { text: 'server-1', value: 'server-1', selected: true }, - { text: 'server-2', value: 'server-2' }, - { text: 'server-3', value: 'server-3' }, - ], - tags: ['key1', 'key2', 'key3'], - getValuesForTag: function(key) { - return q.when(tagValuesMap[key]); - }, - multi: true, - }; - tagValuesMap.key1 = ['server-1', 'server-3']; - tagValuesMap.key2 = ['server-2', 'server-3']; - tagValuesMap.key3 = ['server-1', 'server-2', 'server-3']; - ctrl.init(); - ctrl.show(); - }); - - it('should init tags model', function() { - expect(ctrl.tags.length).to.be(3); - expect(ctrl.tags[0].text).to.be('key1'); - }); - - it('should init options model', function() { - expect(ctrl.options.length).to.be(3); - }); - - it('should init selected values array', function() { - expect(ctrl.selectedValues.length).to.be(1); - }); - - it('should set linkText', function() { - expect(ctrl.linkText).to.be('server-1'); - }); - - describe('after adititional value is selected', function() { - beforeEach(function() { - ctrl.selectValue(ctrl.options[2], {}); - ctrl.commitChanges(); - }); - - it('should update link text', function() { - expect(ctrl.linkText).to.be('server-1 + server-3'); - }); - }); - - describe('When tag is selected', function() { - beforeEach(function() { - ctrl.selectTag(ctrl.tags[0]); - rootScope.$digest(); - ctrl.commitChanges(); - }); - - it('should select tag', function() { - expect(ctrl.selectedTags.length).to.be(1); - }); - - it('should select values', function() { - expect(ctrl.options[0].selected).to.be(true); - expect(ctrl.options[2].selected).to.be(true); - }); - - it('link text should not include tag values', function() { - expect(ctrl.linkText).to.be(''); - }); - - describe('and then dropdown is opened and closed without changes', function() { - beforeEach(function() { - ctrl.show(); - ctrl.commitChanges(); - rootScope.$digest(); - }); - - it('should still have selected tag', function() { - expect(ctrl.selectedTags.length).to.be(1); - }); - }); - - describe('and then unselected', function() { - beforeEach(function() { - ctrl.selectTag(ctrl.tags[0]); - rootScope.$digest(); - }); - - it('should deselect tag', function() { - expect(ctrl.selectedTags.length).to.be(0); - }); - }); - - describe('and then value is unselected', function() { - beforeEach(function() { - ctrl.selectValue(ctrl.options[0], {}); - }); - - it('should deselect tag', function() { - expect(ctrl.selectedTags.length).to.be(0); - }); - }); - }); - }); - - describe('Given variable with selected tags', function() { - beforeEach(function() { - ctrl.variable = { - current: { - text: 'server-1', - value: 'server-1', - tags: [{ text: 'key1', selected: true }], - }, - options: [ - { text: 'server-1', value: 'server-1' }, - { text: 'server-2', value: 'server-2' }, - { text: 'server-3', value: 'server-3' }, - ], - tags: ['key1', 'key2', 'key3'], - getValuesForTag: function(key) { - return q.when(tagValuesMap[key]); - }, - multi: true, - }; - ctrl.init(); - ctrl.show(); - }); - - it('should set tag as selected', function() { - expect(ctrl.tags[0].selected).to.be(true); - }); - }); -}); From 7a7c6f8fab04d622fda04d65fdca1a7914b3f5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 28 Jun 2018 04:38:23 -0700 Subject: [PATCH 06/17] fix: log close/flush was done too early, before server shutdown log message was called, fixes #12438 --- pkg/cmd/grafana-server/main.go | 4 ---- pkg/cmd/grafana-server/server.go | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 976c027d749a3..e64f42a732088 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -14,7 +14,6 @@ import ( "net/http" _ "net/http/pprof" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/setting" @@ -88,9 +87,6 @@ func main() { err := server.Run() - trace.Stop() - log.Close() - server.Exit(err) } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 6444528f7f046..a4543ef1f20db 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -185,6 +185,8 @@ func (g *GrafanaServerImpl) Exit(reason error) { } g.log.Error("Server shutdown", "reason", reason) + + log.Close() os.Exit(code) } From af0a4a60394821b605d03575f3e756c1f0c16b6e Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 28 Jun 2018 13:43:23 +0200 Subject: [PATCH 07/17] Karma to Jest: 3 test files (#12414) * Karma to Jest: viewstate_srv * Karma to Jest: annotations_srv * Remove comments * Karma to Jest: series_override_ctrl * Remove unnecessary code * Class to function and fix lint error * Fix ngInject --- ...s_srv_specs.ts => annotations_srv.jest.ts} | 22 +- .../dashboard/specs/viewstate_srv.jest.ts | 67 +++++ .../dashboard/specs/viewstate_srv_specs.ts | 65 ---- .../panel/graph/series_overrides_ctrl.ts | 280 +++++++++--------- .../graph/specs/series_override_ctrl.jest.ts | 42 +++ .../graph/specs/series_override_ctrl_specs.ts | 55 ---- 6 files changed, 259 insertions(+), 272 deletions(-) rename public/app/features/annotations/specs/{annotations_srv_specs.ts => annotations_srv.jest.ts} (52%) create mode 100644 public/app/features/dashboard/specs/viewstate_srv.jest.ts delete mode 100644 public/app/features/dashboard/specs/viewstate_srv_specs.ts create mode 100644 public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts delete mode 100644 public/app/plugins/panel/graph/specs/series_override_ctrl_specs.ts diff --git a/public/app/features/annotations/specs/annotations_srv_specs.ts b/public/app/features/annotations/specs/annotations_srv.jest.ts similarity index 52% rename from public/app/features/annotations/specs/annotations_srv_specs.ts rename to public/app/features/annotations/specs/annotations_srv.jest.ts index 932fcf9415c74..7db7b6c9f05f3 100644 --- a/public/app/features/annotations/specs/annotations_srv_specs.ts +++ b/public/app/features/annotations/specs/annotations_srv.jest.ts @@ -1,17 +1,17 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import '../annotations_srv'; -import helpers from 'test/specs/helpers'; import 'app/features/dashboard/time_srv'; +import { AnnotationsSrv } from '../annotations_srv'; describe('AnnotationsSrv', function() { - var ctx = new helpers.ServiceTestContext(); + let $rootScope = { + onAppEvent: jest.fn(), + }; + let $q; + let datasourceSrv; + let backendSrv; + let timeSrv; - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(ctx.createService('timeSrv')); - beforeEach(() => { - ctx.createService('annotationsSrv'); - }); + let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); describe('When translating the query result', () => { const annotationSource = { @@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() { let translatedAnnotations; beforeEach(() => { - translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations); + translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations); }); it('should set defaults', () => { - expect(translatedAnnotations[0].source).to.eql(annotationSource); + expect(translatedAnnotations[0].source).toEqual(annotationSource); }); }); }); diff --git a/public/app/features/dashboard/specs/viewstate_srv.jest.ts b/public/app/features/dashboard/specs/viewstate_srv.jest.ts new file mode 100644 index 0000000000000..08166c6f2bd66 --- /dev/null +++ b/public/app/features/dashboard/specs/viewstate_srv.jest.ts @@ -0,0 +1,67 @@ +//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; +import 'app/features/dashboard/view_state_srv'; +import config from 'app/core/config'; +import { DashboardViewState } from '../view_state_srv'; + +describe('when updating view state', () => { + let location = { + replace: jest.fn(), + search: jest.fn(), + }; + + let $scope = { + onAppEvent: jest.fn(() => {}), + dashboard: { + meta: {}, + panels: [], + }, + }; + + let $rootScope = {}; + let viewState; + + beforeEach(() => { + config.bootData = { + user: { + orgId: 1, + }, + }; + }); + + describe('to fullscreen true and edit true', () => { + beforeEach(() => { + location.search = jest.fn(() => { + return { fullscreen: true, edit: true, panelId: 1 }; + }); + viewState = new DashboardViewState($scope, location, {}, $rootScope); + }); + + it('should update querystring and view state', () => { + var updateState = { fullscreen: true, edit: true, panelId: 1 }; + + viewState.update(updateState); + + expect(location.search).toHaveBeenCalledWith({ + edit: true, + editview: null, + fullscreen: true, + orgId: 1, + panelId: 1, + }); + expect(viewState.dashboard.meta.fullscreen).toBe(true); + expect(viewState.state.fullscreen).toBe(true); + }); + }); + + describe('to fullscreen false', () => { + beforeEach(() => { + viewState = new DashboardViewState($scope, location, {}, $rootScope); + }); + it('should remove params from query string', () => { + viewState.update({ fullscreen: true, panelId: 1, edit: true }); + viewState.update({ fullscreen: false }); + expect(viewState.dashboard.meta.fullscreen).toBe(false); + expect(viewState.state.fullscreen).toBe(null); + }); + }); +}); diff --git a/public/app/features/dashboard/specs/viewstate_srv_specs.ts b/public/app/features/dashboard/specs/viewstate_srv_specs.ts deleted file mode 100644 index d34b15b9113e5..0000000000000 --- a/public/app/features/dashboard/specs/viewstate_srv_specs.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import 'app/features/dashboard/view_state_srv'; -import config from 'app/core/config'; - -describe('when updating view state', function() { - var viewState, location; - var timeSrv = {}; - var templateSrv = {}; - var contextSrv = { - user: { - orgId: 19, - }, - }; - beforeEach(function() { - config.bootData = { - user: { - orgId: 1, - }, - }; - }); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.module(function($provide) { - $provide.value('timeSrv', timeSrv); - $provide.value('templateSrv', templateSrv); - $provide.value('contextSrv', contextSrv); - }) - ); - - beforeEach( - angularMocks.inject(function(dashboardViewStateSrv, $location, $rootScope) { - $rootScope.onAppEvent = function() {}; - $rootScope.dashboard = { - meta: {}, - panels: [], - }; - viewState = dashboardViewStateSrv.create($rootScope); - location = $location; - }) - ); - - describe('to fullscreen true and edit true', function() { - it('should update querystring and view state', function() { - var updateState = { fullscreen: true, edit: true, panelId: 1 }; - viewState.update(updateState); - expect(location.search()).to.eql({ - fullscreen: true, - edit: true, - panelId: 1, - orgId: 1, - }); - expect(viewState.dashboard.meta.fullscreen).to.be(true); - expect(viewState.state.fullscreen).to.be(true); - }); - }); - - describe('to fullscreen false', function() { - it('should remove params from query string', function() { - viewState.update({ fullscreen: true, panelId: 1, edit: true }); - viewState.update({ fullscreen: false }); - expect(viewState.dashboard.meta.fullscreen).to.be(false); - expect(viewState.state.fullscreen).to.be(null); - }); - }); -}); diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index ecf79a8a4fb55..5958c80bac9af 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -1,160 +1,158 @@ import _ from 'lodash'; import angular from 'angular'; -export class SeriesOverridesCtrl { - /** @ngInject */ - constructor($scope, $element, popoverSrv) { - $scope.overrideMenu = []; - $scope.currentOverrides = []; - $scope.override = $scope.override || {}; - - $scope.addOverrideOption = function(name, propertyName, values) { - var option = { - text: name, - propertyName: propertyName, - index: $scope.overrideMenu.lenght, - values: values, - submenu: _.map(values, function(value) { - return { text: String(value), value: value }; - }), - }; +/** @ngInject */ +export function SeriesOverridesCtrl($scope, $element, popoverSrv) { + $scope.overrideMenu = []; + $scope.currentOverrides = []; + $scope.override = $scope.override || {}; - $scope.overrideMenu.push(option); + $scope.addOverrideOption = function(name, propertyName, values) { + var option = { + text: name, + propertyName: propertyName, + index: $scope.overrideMenu.lenght, + values: values, + submenu: _.map(values, function(value) { + return { text: String(value), value: value }; + }), }; - $scope.setOverride = function(item, subItem) { - // handle color overrides - if (item.propertyName === 'color') { - $scope.openColorSelector($scope.override['color']); - return; - } + $scope.overrideMenu.push(option); + }; - $scope.override[item.propertyName] = subItem.value; + $scope.setOverride = function(item, subItem) { + // handle color overrides + if (item.propertyName === 'color') { + $scope.openColorSelector($scope.override['color']); + return; + } - // automatically disable lines for this series and the fill below to series - // can be removed by the user if they still want lines - if (item.propertyName === 'fillBelowTo') { - $scope.override['lines'] = false; - $scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false }); - } + $scope.override[item.propertyName] = subItem.value; - $scope.updateCurrentOverrides(); - $scope.ctrl.render(); - }; + // automatically disable lines for this series and the fill below to series + // can be removed by the user if they still want lines + if (item.propertyName === 'fillBelowTo') { + $scope.override['lines'] = false; + $scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false }); + } - $scope.colorSelected = function(color) { - $scope.override['color'] = color; - $scope.updateCurrentOverrides(); - $scope.ctrl.render(); - }; + $scope.updateCurrentOverrides(); + $scope.ctrl.render(); + }; - $scope.openColorSelector = function(color) { - var fakeSeries = { color: color }; - popoverSrv.show({ - element: $element.find('.dropdown')[0], - position: 'top center', - openOn: 'click', - template: '', - model: { - autoClose: true, - colorSelected: $scope.colorSelected, - series: fakeSeries, - }, - onClose: function() { - $scope.ctrl.render(); - }, - }); - }; + $scope.colorSelected = function(color) { + $scope.override['color'] = color; + $scope.updateCurrentOverrides(); + $scope.ctrl.render(); + }; - $scope.removeOverride = function(option) { - delete $scope.override[option.propertyName]; - $scope.updateCurrentOverrides(); - $scope.ctrl.refresh(); - }; + $scope.openColorSelector = function(color) { + var fakeSeries = { color: color }; + popoverSrv.show({ + element: $element.find('.dropdown')[0], + position: 'top center', + openOn: 'click', + template: '', + model: { + autoClose: true, + colorSelected: $scope.colorSelected, + series: fakeSeries, + }, + onClose: function() { + $scope.ctrl.render(); + }, + }); + }; - $scope.getSeriesNames = function() { - return _.map($scope.ctrl.seriesList, function(series) { - return series.alias; - }); - }; + $scope.removeOverride = function(option) { + delete $scope.override[option.propertyName]; + $scope.updateCurrentOverrides(); + $scope.ctrl.refresh(); + }; + + $scope.getSeriesNames = function() { + return _.map($scope.ctrl.seriesList, function(series) { + return series.alias; + }); + }; - $scope.updateCurrentOverrides = function() { - $scope.currentOverrides = []; - _.each($scope.overrideMenu, function(option) { - var value = $scope.override[option.propertyName]; - if (_.isUndefined(value)) { - return; - } - $scope.currentOverrides.push({ - name: option.text, - propertyName: option.propertyName, - value: String(value), - }); + $scope.updateCurrentOverrides = function() { + $scope.currentOverrides = []; + _.each($scope.overrideMenu, function(option) { + var value = $scope.override[option.propertyName]; + if (_.isUndefined(value)) { + return; + } + $scope.currentOverrides.push({ + name: option.text, + propertyName: option.propertyName, + value: String(value), }); - }; + }); + }; - $scope.addOverrideOption('Bars', 'bars', [true, false]); - $scope.addOverrideOption('Lines', 'lines', [true, false]); - $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']); - $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames()); - $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); - $scope.addOverrideOption('Dashes', 'dashes', [true, false]); - $scope.addOverrideOption('Dash Length', 'dashLength', [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - ]); - $scope.addOverrideOption('Dash Space', 'spaceLength', [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - ]); - $scope.addOverrideOption('Points', 'points', [true, false]); - $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]); - $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']); - $scope.addOverrideOption('Color', 'color', ['change']); - $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); - $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]); - $scope.addOverrideOption('Transform', 'transform', ['negative-Y']); - $scope.addOverrideOption('Legend', 'legend', [true, false]); - $scope.updateCurrentOverrides(); - } + $scope.addOverrideOption('Bars', 'bars', [true, false]); + $scope.addOverrideOption('Lines', 'lines', [true, false]); + $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']); + $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames()); + $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); + $scope.addOverrideOption('Dashes', 'dashes', [true, false]); + $scope.addOverrideOption('Dash Length', 'dashLength', [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + ]); + $scope.addOverrideOption('Dash Space', 'spaceLength', [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + ]); + $scope.addOverrideOption('Points', 'points', [true, false]); + $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]); + $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']); + $scope.addOverrideOption('Color', 'color', ['change']); + $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); + $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]); + $scope.addOverrideOption('Transform', 'transform', ['negative-Y']); + $scope.addOverrideOption('Legend', 'legend', [true, false]); + $scope.updateCurrentOverrides(); } angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); diff --git a/public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts b/public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts new file mode 100644 index 0000000000000..2e7456a132a49 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts @@ -0,0 +1,42 @@ +import '../series_overrides_ctrl'; +import { SeriesOverridesCtrl } from '../series_overrides_ctrl'; + +describe('SeriesOverridesCtrl', () => { + let popoverSrv = {}; + let $scope; + + beforeEach(() => { + $scope = { + ctrl: { + refresh: jest.fn(), + render: jest.fn(), + seriesList: [], + }, + render: jest.fn(() => {}), + }; + SeriesOverridesCtrl($scope, {}, popoverSrv); + }); + + describe('When setting an override', () => { + beforeEach(() => { + $scope.setOverride({ propertyName: 'lines' }, { value: true }); + }); + + it('should set override property', () => { + expect($scope.override.lines).toBe(true); + }); + + it('should update view model', () => { + expect($scope.currentOverrides[0].name).toBe('Lines'); + expect($scope.currentOverrides[0].value).toBe('true'); + }); + }); + + describe('When removing overide', () => { + it('click should include option and value index', () => { + $scope.setOverride(1, 0); + $scope.removeOverride({ propertyName: 'lines' }); + expect($scope.currentOverrides.length).toBe(0); + }); + }); +}); diff --git a/public/app/plugins/panel/graph/specs/series_override_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/series_override_ctrl_specs.ts deleted file mode 100644 index 9e311c0775ef0..0000000000000 --- a/public/app/plugins/panel/graph/specs/series_override_ctrl_specs.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common'; -import '../series_overrides_ctrl'; -import helpers from 'test/specs/helpers'; - -describe('SeriesOverridesCtrl', function() { - var ctx = new helpers.ControllerTestContext(); - var popoverSrv = {}; - - beforeEach(angularMocks.module('grafana.services')); - beforeEach(angularMocks.module('grafana.controllers')); - - beforeEach( - ctx.providePhase({ - popoverSrv: popoverSrv, - }) - ); - - beforeEach( - angularMocks.inject(function($rootScope, $controller) { - ctx.scope = $rootScope.$new(); - ctx.scope.ctrl = { - refresh: sinon.spy(), - render: sinon.spy(), - seriesList: [], - }; - ctx.scope.render = function() {}; - ctx.controller = $controller('SeriesOverridesCtrl', { - $scope: ctx.scope, - }); - }) - ); - - describe('When setting an override', function() { - beforeEach(function() { - ctx.scope.setOverride({ propertyName: 'lines' }, { value: true }); - }); - - it('should set override property', function() { - expect(ctx.scope.override.lines).to.be(true); - }); - - it('should update view model', function() { - expect(ctx.scope.currentOverrides[0].name).to.be('Lines'); - expect(ctx.scope.currentOverrides[0].value).to.be('true'); - }); - }); - - describe('When removing overide', function() { - it('click should include option and value index', function() { - ctx.scope.setOverride(1, 0); - ctx.scope.removeOverride({ propertyName: 'lines' }); - expect(ctx.scope.currentOverrides.length).to.be(0); - }); - }); -}); From 54420363d3640570dafdcba24afda5b6fdc04c1d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 28 Jun 2018 14:02:42 +0200 Subject: [PATCH 08/17] fix footer css issue --- public/sass/components/_footer.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/sass/components/_footer.scss b/public/sass/components/_footer.scss index 4a77ec37605bd..3c30f5a57988f 100644 --- a/public/sass/components/_footer.scss +++ b/public/sass/components/_footer.scss @@ -45,9 +45,11 @@ } @include media-breakpoint-up(md) { - .footer { - bottom: $spacer; - position: absolute; - padding: 5rem 0 1rem 0; + .login-page { + .footer { + bottom: $spacer; + position: absolute; + padding: 5rem 0 1rem 0; + } } } From 4c4bd2ebba839b6d043bc69606b9977fef3efe39 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 28 Jun 2018 14:27:41 +0200 Subject: [PATCH 09/17] changelog: add notes about closing #12430 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c943b2b97767b..e149b42035385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) +# 5.2.1 (unreleased) + +### Minor + +* **UI**: Fix - Grafana footer overlapping page [#12430](https://github.com/grafana/grafana/issues/12430) + # 5.2.0-stable (2018-06-27) ### Minor From 7a2be69abcd07d22d48815e554e02a25bea14a22 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 28 Jun 2018 14:46:40 +0200 Subject: [PATCH 10/17] changelog: adds note for #11892 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e149b42035385..7be0564bc9676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) +* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) # 5.2.1 (unreleased) From 8af5da738340e55ded1ee5e1a3b14287264d0451 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 28 Jun 2018 15:43:33 +0200 Subject: [PATCH 11/17] Revert "auth proxy: use real ip when validating white listed ip's" --- pkg/middleware/auth_proxy.go | 20 +++++------ pkg/middleware/middleware_test.go | 55 ------------------------------- 2 files changed, 8 insertions(+), 67 deletions(-) diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index eff532b0da2d2..144a0ae3a693e 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -2,6 +2,7 @@ package middleware import ( "fmt" + "net" "net/mail" "reflect" "strings" @@ -28,7 +29,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { } // if auth proxy ip(s) defined, check if request comes from one of those - if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil { + if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil { ctx.Handle(407, "Proxy authentication required", err) return true } @@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error return nil } - // Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy - if strings.Contains(remoteAddr, ",") { - sourceIPs := strings.Split(remoteAddr, ",") - remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1]) - } - - remoteAddr = strings.TrimPrefix(remoteAddr, "[") - remoteAddr = strings.TrimSuffix(remoteAddr, "]") - proxies := strings.Split(setting.AuthProxyWhitelist, ",") + sourceIP, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + return err + } // Compare allowed IP addresses to actual address for _, proxyIP := range proxies { - if remoteAddr == strings.TrimSpace(proxyIP) { + if sourceIP == strings.TrimSpace(proxyIP) { return nil } } - return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr) + return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP) } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index d82b7313585c4..87c23a7b49a92 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -315,61 +315,6 @@ func TestMiddlewareContext(t *testing.T) { }) }) - middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is not trusted", func(sc *scenarioContext) { - setting.AuthProxyEnabled = true - setting.AuthProxyHeaderName = "X-WEBAUTH-USER" - setting.AuthProxyHeaderProperty = "username" - setting.AuthProxyWhitelist = "192.168.1.1, 2001::23" - - bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { - query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} - return nil - }) - - bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { - cmd.Result = &m.User{Id: 33} - return nil - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") - sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.1, 192.168.1.2") - sc.exec() - - Convey("should return 407 status code", func() { - So(sc.resp.Code, ShouldEqual, 407) - So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.1.2 is not from the authentication proxy") - }) - }) - - middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is trusted", func(sc *scenarioContext) { - setting.AuthProxyEnabled = true - setting.AuthProxyHeaderName = "X-WEBAUTH-USER" - setting.AuthProxyHeaderProperty = "username" - setting.AuthProxyWhitelist = "192.168.1.1, 2001::23" - - bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { - query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} - return nil - }) - - bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { - cmd.Result = &m.User{Id: 33} - return nil - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") - sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.2, 192.168.1.1") - sc.exec() - - Convey("Should init context with user info", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 33) - So(sc.context.OrgId, ShouldEqual, 4) - }) - }) - middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) { setting.AuthProxyEnabled = true setting.AuthProxyHeaderName = "X-WEBAUTH-USER" From fb2b2c9f656f9d8f894edea70418b78efd3f22f3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 28 Jun 2018 16:39:38 +0200 Subject: [PATCH 12/17] changelog: add notes about closing #12444 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be0564bc9676..aaa9dec5c4d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Minor * **UI**: Fix - Grafana footer overlapping page [#12430](https://github.com/grafana/grafana/issues/12430) +* **Auth Proxy**: Revert of "Whitelist proxy IP address instead of client IP address" introduced in 5.2.0-beta2 [#12444](https://github.com/grafana/grafana/pull/12444) # 5.2.0-stable (2018-06-27) From ce88e4b927a4d417c91829e2e0bc227756ebb915 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 29 Jun 2018 09:35:31 +0200 Subject: [PATCH 13/17] fixes typos closes #12453 --- CHANGELOG.md | 2 +- public/app/partials/login.html | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaa9dec5c4d40..fcb1cf6d19d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1336,7 +1336,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated **New features** - [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site - [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site -- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is promted with a warning if he really wants to overwrite the other's changes +- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is prompted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views - [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, useful when you want to ignore last minute because it contains incomplete data - [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 8be9e777b9fa9..1919759334bc7 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -89,7 +89,7 @@
Change Password
Skip - If you skip you will be promted to change password next time you login. + If you skip you will be prompted to change password next time you login.