diff --git a/conf/defaults.ini b/conf/defaults.ini index 81b32540198..b609005aba0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -823,4 +823,5 @@ interval_year = YYYY # Experimental feature use_browser_locale = false - +# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc. +default_timezone = browser diff --git a/conf/sample.ini b/conf/sample.ini index f0254e24ed4..95d7dda6a72 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -811,3 +811,6 @@ # Experimental feature ;use_browser_locale = false + +# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc. +;default_timezone = browser \ No newline at end of file diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index f7b978726d7..49ff190313e 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -1426,3 +1426,6 @@ interval_year = YYYY Set this to `true` to have date formats be automatically be derived from browser locale. Defaults to `false`. This is an experimental feature right now with a few problems that remain unsolved. +### default_timezone + +Used as the default timezone for user preferences. Can be either `browser` for the browser local timezone or a timezone name from IANA Time Zone database, e.g. `UTC` or `Europe/Amsterdam` etc. \ No newline at end of file diff --git a/docs/sources/administration/preferences.md b/docs/sources/administration/preferences.md index 0bb5176d620..f71e7cca9f4 100644 --- a/docs/sources/administration/preferences.md +++ b/docs/sources/administration/preferences.md @@ -32,7 +32,7 @@ Your Grafana preferences include whether uses the dark or light theme, your home 1. In the Preferences section, you can edit any of the following: - **UI Theme -** Click to set the **Dark** or **Light** to select a theme. **Default** is either the dark theme or the theme selected by your Grafana administrator. - **Home Dashboard -** Refer to [Set your personal home dashboard]({{< relref "change-home-dashboard.md#set-your-personal-home-dashboard" >}}) for more information. - - **Timezone -** Click to select an option in the **Timezone** list. Refer to [Time range controls]({{< relref "../dashboards/time-range-controls.md" >}}) for more information about Grafana time settings. + - **Timezone -** Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected by your Grafana administrator. Refer to [Time range controls]({{< relref "../dashboards/time-range-controls.md" >}}) for more information about Grafana time settings. 1. Click **Save**. ## View your assigned organizations diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 4513a2b20f7..bb665ebcf3b 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -10,13 +10,13 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func init() { +func (ss *SqlStore) addPreferencesQueryAndCommandHandlers() { bus.AddHandler("sql", GetPreferences) - bus.AddHandler("sql", GetPreferencesWithDefaults) + bus.AddHandler("sql", ss.GetPreferencesWithDefaults) bus.AddHandler("sql", SavePreferences) } -func GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) error { +func (ss *SqlStore) GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) error { params := make([]interface{}, 0) filter := "" @@ -43,7 +43,7 @@ func GetPreferencesWithDefaults(query *models.GetPreferencesWithDefaultsQuery) e res := &models.Preferences{ Theme: setting.DefaultTheme, - Timezone: "browser", + Timezone: ss.Cfg.DateFormats.DefaultTimezone, HomeDashboardId: 0, } diff --git a/pkg/services/sqlstore/preferences_test.go b/pkg/services/sqlstore/preferences_test.go index 43e1e96d930..841a317a9a1 100644 --- a/pkg/services/sqlstore/preferences_test.go +++ b/pkg/services/sqlstore/preferences_test.go @@ -11,14 +11,17 @@ import ( func TestPreferencesDataAccess(t *testing.T) { Convey("Testing preferences data access", t, func() { - InitTestDB(t) + ss := InitTestDB(t) Convey("GetPreferencesWithDefaults with no saved preferences should return defaults", func() { + setting.DefaultTheme = "light" + ss.Cfg.DateFormats.DefaultTimezone = "UTC" + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{}} - err := GetPreferencesWithDefaults(query) + err := ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) - So(query.Result.Theme, ShouldEqual, setting.DefaultTheme) - So(query.Result.Timezone, ShouldEqual, "browser") + So(query.Result.Theme, ShouldEqual, "light") + So(query.Result.Timezone, ShouldEqual, "UTC") So(query.Result.HomeDashboardId, ShouldEqual, 0) }) @@ -29,7 +32,7 @@ func TestPreferencesDataAccess(t *testing.T) { So(err, ShouldBeNil) query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1}} - err = GetPreferencesWithDefaults(query) + err = ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) So(query.Result.HomeDashboardId, ShouldEqual, 4) }) @@ -41,7 +44,7 @@ func TestPreferencesDataAccess(t *testing.T) { So(err, ShouldBeNil) query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}} - err = GetPreferencesWithDefaults(query) + err = ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) So(query.Result.HomeDashboardId, ShouldEqual, 1) }) @@ -57,7 +60,7 @@ func TestPreferencesDataAccess(t *testing.T) { query := &models.GetPreferencesWithDefaultsQuery{ User: &models.SignedInUser{OrgId: 1, Teams: []int64{2, 3}}, } - err = GetPreferencesWithDefaults(query) + err = ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) So(query.Result.HomeDashboardId, ShouldEqual, 3) }) @@ -71,7 +74,7 @@ func TestPreferencesDataAccess(t *testing.T) { So(err, ShouldBeNil) query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1}} - err = GetPreferencesWithDefaults(query) + err = ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) So(query.Result.HomeDashboardId, ShouldEqual, 1) }) @@ -89,7 +92,7 @@ func TestPreferencesDataAccess(t *testing.T) { query := &models.GetPreferencesWithDefaultsQuery{ User: &models.SignedInUser{OrgId: 1, UserId: 1, Teams: []int64{2, 3}}, } - err = GetPreferencesWithDefaults(query) + err = ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) So(query.Result.HomeDashboardId, ShouldEqual, 4) }) @@ -107,7 +110,7 @@ func TestPreferencesDataAccess(t *testing.T) { query := &models.GetPreferencesWithDefaultsQuery{ User: &models.SignedInUser{OrgId: 1, UserId: 2}, } - err = GetPreferencesWithDefaults(query) + err = ss.GetPreferencesWithDefaults(query) So(err, ShouldBeNil) So(query.Result.HomeDashboardId, ShouldEqual, 1) }) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 406d2e194db..d77e4400670 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -102,6 +102,7 @@ func (ss *SqlStore) Init() error { // Register handlers ss.addUserQueryAndCommandHandlers() ss.addAlertNotificationUidByIdHandler() + ss.addPreferencesQueryAndCommandHandlers() err = ss.logOrgsNotice() if err != nil { diff --git a/pkg/setting/date_formats.go b/pkg/setting/date_formats.go index cc214759f6c..54aa02bb703 100644 --- a/pkg/setting/date_formats.go +++ b/pkg/setting/date_formats.go @@ -1,9 +1,16 @@ package setting +import ( + "time" + + "gopkg.in/ini.v1" +) + type DateFormats struct { FullDate string `json:"fullDate"` UseBrowserLocale bool `json:"useBrowserLocale"` Interval DateFormatIntervals `json:"interval"` + DefaultTimezone string `json:"defaultTimezone"` } type DateFormatIntervals struct { @@ -15,6 +22,23 @@ type DateFormatIntervals struct { Year string `json:"year"` } +const LocalBrowserTimezone = "browser" + +func valueAsTimezone(section *ini.Section, keyName string, defaultValue string) (string, error) { + timezone := section.Key(keyName).MustString(defaultValue) + + if timezone == LocalBrowserTimezone { + return LocalBrowserTimezone, nil + } + + location, err := time.LoadLocation(timezone) + if err != nil { + return LocalBrowserTimezone, err + } + + return location.String(), nil +} + func (cfg *Cfg) readDateFormats() { dateFormats := cfg.Raw.Section("date_formats") cfg.DateFormats.FullDate = valueAsString(dateFormats, "full_date", "YYYY-MM-DD HH:mm:ss") @@ -25,4 +49,10 @@ func (cfg *Cfg) readDateFormats() { cfg.DateFormats.Interval.Month = valueAsString(dateFormats, "interval_month", "YYYY-MM") cfg.DateFormats.Interval.Year = "YYYY" cfg.DateFormats.UseBrowserLocale = dateFormats.Key("date_format_use_browser_locale").MustBool(false) + + timezone, err := valueAsTimezone(dateFormats, "default_timezone", LocalBrowserTimezone) + if err != nil { + cfg.Logger.Warn("Unknown timezone as default_timezone", "err", err) + } + cfg.DateFormats.DefaultTimezone = timezone } diff --git a/pkg/setting/date_formats_test.go b/pkg/setting/date_formats_test.go new file mode 100644 index 00000000000..a0701207384 --- /dev/null +++ b/pkg/setting/date_formats_test.go @@ -0,0 +1,37 @@ +package setting + +import ( + "testing" + + "gopkg.in/ini.v1" + + "github.com/stretchr/testify/assert" +) + +func TestValueAsTimezone(t *testing.T) { + tests := map[string]struct { + output string + hasErr bool + }{ + "browser": {"browser", false}, + "UTC": {"UTC", false}, + "utc": {"browser", true}, + "Amsterdam": {"browser", true}, + "europe/amsterdam": {"browser", true}, + "Europe/Amsterdam": {"Europe/Amsterdam", false}, + } + + sec, err := ini.Empty().NewSection("test") + assert.NoError(t, err) + key, err := sec.NewKey("test", "") + assert.NoError(t, err) + + for input, expected := range tests { + key.SetValue(input) + + output, err := valueAsTimezone(sec, "test", "default") + + assert.Equal(t, expected.hasErr, err != nil, "Invalid has err for input: %s err: %v", input, err) + assert.Equal(t, expected.output, output, "Invalid output for input: %s", input) + } +}