Moved dashboard theme option from the dashboard to a persisted user setting, #1458

This commit is contained in:
Torkel Ödegaard 2015-02-28 14:30:08 +01:00
parent 962b316bcf
commit 04ca85fe89
15 changed files with 56 additions and 35 deletions

View File

@ -5,6 +5,7 @@
- [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``, usefull when you want to ignore last minute because it contains incomplete data - [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``, usefull 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 - [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
- [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as - [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
- [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme (no longer an option on a dashboard)
**Enhancements** **Enhancements**
- [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot) - [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot)

View File

@ -28,7 +28,7 @@ func AdminGetUser(c *middleware.Context) {
return return
} }
result := m.UserDTO{ result := dtos.AdminUserListItem{
Name: query.Result.Name, Name: query.Result.Name,
Email: query.Result.Email, Email: query.Result.Email,
Login: query.Result.Login, Login: query.Result.Login,

View File

@ -19,6 +19,7 @@ type CurrentUser struct {
Login string `json:"login"` Login string `json:"login"`
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgRole m.RoleType `json:"orgRole"` OrgRole m.RoleType `json:"orgRole"`
OrgName string `json:"orgName"` OrgName string `json:"orgName"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"`

View File

@ -20,3 +20,10 @@ type AdminUpdateUserPasswordForm struct {
type AdminUpdateUserPermissionsForm struct { type AdminUpdateUserPermissionsForm struct {
IsGrafanaAdmin bool `json:"IsGrafanaAdmin" binding:"Required"` IsGrafanaAdmin bool `json:"IsGrafanaAdmin" binding:"Required"`
} }
type AdminUserListItem struct {
Email string `json:"email"`
Name string `json:"name"`
Login string `json:"login"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
}

View File

@ -17,6 +17,7 @@ func setIndexViewData(c *middleware.Context) error {
Login: c.Login, Login: c.Login,
Email: c.Email, Email: c.Email,
Name: c.Name, Name: c.Name,
LightTheme: c.Theme == "light",
OrgName: c.OrgName, OrgName: c.OrgName,
OrgRole: c.OrgRole, OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email), GravatarUrl: dtos.GetGravatarUrl(c.Email),

View File

@ -8,7 +8,7 @@ import (
) )
func GetUser(c *middleware.Context) { func GetUser(c *middleware.Context) {
query := m.GetUserInfoQuery{UserId: c.UserId} query := m.GetUserProfileQuery{UserId: c.UserId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to get user", err) c.JsonApiErr(500, "Failed to get user", err)

View File

@ -48,6 +48,7 @@ type UpdateUserCommand struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Login string `json:"login"` Login string `json:"login"`
Theme string `json:"theme"`
UserId int64 `json:"-"` UserId int64 `json:"-"`
} }
@ -91,9 +92,9 @@ type GetSignedInUserQuery struct {
Result *SignedInUser Result *SignedInUser
} }
type GetUserInfoQuery struct { type GetUserProfileQuery struct {
UserId int64 UserId int64
Result UserDTO Result UserProfileDTO
} }
type SearchUsersQuery struct { type SearchUsersQuery struct {
@ -120,14 +121,16 @@ type SignedInUser struct {
Login string Login string
Name string Name string
Email string Email string
Theme string
ApiKeyId int64 ApiKeyId int64
IsGrafanaAdmin bool IsGrafanaAdmin bool
} }
type UserDTO struct { type UserProfileDTO struct {
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
Login string `json:"login"` Login string `json:"login"`
Theme string `json:"theme"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
} }

View File

@ -53,8 +53,8 @@ func TestAccountDataAccess(t *testing.T) {
ac2 := ac2cmd.Result ac2 := ac2cmd.Result
Convey("Should be able to read user info projection", func() { Convey("Should be able to read user info projection", func() {
query := m.GetUserInfoQuery{UserId: ac1.Id} query := m.GetUserProfileQuery{UserId: ac1.Id}
err = GetUserInfo(&query) err = GetUserProfile(&query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result.Email, ShouldEqual, "ac1@test.com") So(query.Result.Email, ShouldEqual, "ac1@test.com")

View File

@ -21,7 +21,7 @@ func init() {
bus.AddHandler("sql", ChangeUserPassword) bus.AddHandler("sql", ChangeUserPassword)
bus.AddHandler("sql", GetUserByLogin) bus.AddHandler("sql", GetUserByLogin)
bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", SetUsingOrg)
bus.AddHandler("sql", GetUserInfo) bus.AddHandler("sql", GetUserProfile)
bus.AddHandler("sql", GetSignedInUser) bus.AddHandler("sql", GetSignedInUser)
bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", SearchUsers)
bus.AddHandler("sql", GetUserOrgList) bus.AddHandler("sql", GetUserOrgList)
@ -165,6 +165,7 @@ func UpdateUser(cmd *m.UpdateUserCommand) error {
Name: cmd.Name, Name: cmd.Name,
Email: cmd.Email, Email: cmd.Email,
Login: cmd.Login, Login: cmd.Login,
Theme: cmd.Theme,
Updated: time.Now(), Updated: time.Now(),
} }
@ -211,7 +212,7 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
}) })
} }
func GetUserInfo(query *m.GetUserInfoQuery) error { func GetUserProfile(query *m.GetUserProfileQuery) error {
var user m.User var user m.User
has, err := x.Id(query.UserId).Get(&user) has, err := x.Id(query.UserId).Get(&user)
@ -221,10 +222,11 @@ func GetUserInfo(query *m.GetUserInfoQuery) error {
return m.ErrUserNotFound return m.ErrUserNotFound
} }
query.Result = m.UserDTO{ query.Result = m.UserProfileDTO{
Name: user.Name, Name: user.Name,
Email: user.Email, Email: user.Email,
Login: user.Login, Login: user.Login,
Theme: user.Theme,
} }
return err return err
@ -247,6 +249,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
u.email as email, u.email as email,
u.login as login, u.login as login,
u.name as name, u.name as name,
u.theme as theme,
org.name as org_name, org.name as org_name,
org_user.role as org_role, org_user.role as org_role,
org.id as org_id org.id as org_id

View File

@ -65,11 +65,6 @@ function (angular, $, config) {
$scope.setWindowTitleAndTheme = function() { $scope.setWindowTitleAndTheme = function() {
window.document.title = config.window_title_prefix + $scope.dashboard.title; window.document.title = config.window_title_prefix + $scope.dashboard.title;
$scope.contextSrv.lightTheme = $scope.dashboard.style === 'light';
};
$scope.styleUpdated = function() {
$scope.contextSrv.lightTheme = $scope.dashboard.style === 'light';
}; };
$scope.broadcastRefresh = function() { $scope.broadcastRefresh = function() {

View File

@ -24,7 +24,6 @@ function (angular, $) {
$scope.initPanelScope = function(dashboard) { $scope.initPanelScope = function(dashboard) {
$scope.dashboard = dashboardSrv.create(dashboard.model); $scope.dashboard = dashboardSrv.create(dashboard.model);
$scope.contextSrv.lightTheme = $scope.dashboard.style === 'light';
$scope.row = { $scope.row = {
height: $(window).height() + 'px', height: $(window).height() + 'px',

View File

@ -1,4 +1,4 @@
<topnav title="{{contextSrv.user.name}}" section="Profile" icon="fa fw fa-user" subnav="true"> <topnav title="{{contextSrv.user.name}}" section="Profile" icon="fa fa-fw fa-user" subnav="true">
<ul class="nav"> <ul class="nav">
<li class="active"><a href="profile">Overview</a></li> <li class="active"><a href="profile">Overview</a></li>
<li><a href="profile/password">Change password</a></li> <li><a href="profile/password">Change password</a></li>
@ -8,14 +8,14 @@
<div class="page-container"> <div class="page-container">
<div class="page"> <div class="page">
<h2>Personal information</h2> <h2>Profile details</h2>
<form name="userForm"> <form name="userForm">
<div> <div>
<div class="tight-form"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px"> <li class="tight-form-item" style="width: 100px">
<strong>Name</strong> Name
</li> </li>
<li> <li>
<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" > <input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
@ -23,10 +23,10 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form" style="margin-top: 5px"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px"> <li class="tight-form-item" style="width: 100px">
<strong>Email</strong> Email
</li> </li>
<li> <li>
<input type="email" required ng-model="user.email" class="input-xxlarge tight-form-input last" > <input type="email" required ng-model="user.email" class="input-xxlarge tight-form-input last" >
@ -34,10 +34,10 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form" style="margin-top: 5px"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px"> <li class="tight-form-item" style="width: 100px">
<strong>Username</strong> Username
</li> </li>
<li> <li>
<input type="text" required ng-model="user.login" class="input-xxlarge tight-form-input last" > <input type="text" required ng-model="user.login" class="input-xxlarge tight-form-input last" >
@ -45,23 +45,36 @@
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
UI Theme
</li>
<li>
<select class="input-small tight-form-input" ng-model="user.theme" ng-options="f for f in ['dark', 'light']" ng-change="themeChanged()"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div> </div>
<br> <br>
<button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button> <button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button>
</form> </form>
<h2>Your Organizations</h2> <h2>Organizations</h2>
<table class="grafana-options-table"> <table class="grafana-options-table">
<tr ng-repeat="org in orgs"> <tr ng-repeat="org in orgs">
<td style="width: 98%"><strong>Name: </strong> {{org.name}}</td> <td style="width: 98%"><strong>Name: </strong> {{org.name}}</td>
<td><strong>Role: </strong> {{org.role}}</td> <td><strong>Role: </strong> {{org.role}}</td>
<td class="nobg max-width-btns"> <td class="nobg max-width-btns">
<span class="btn btn-primary" ng-show="org.isUsing"> <span class="btn btn-primary btn-mini" ng-show="org.isUsing">
Current Current
</span> </span>
<a ng-click="setUsingOrg(org)" class="btn btn-inverse" ng-show="!org.isUsing"> <a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="!org.isUsing">
Select Select
</a> </a>
</td> </td>

View File

@ -17,6 +17,7 @@ function (angular, config) {
$scope.getUser = function() { $scope.getUser = function() {
backendSrv.get('/api/user').then(function(user) { backendSrv.get('/api/user').then(function(user) {
$scope.user = user; $scope.user = user;
$scope.user.theme = user.theme || 'dark';
}); });
}; };

View File

@ -25,9 +25,6 @@
<div class="editor-option"> <div class="editor-option">
<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input> <label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
</div> </div>
<div class="editor-option">
<label class="small">Theme</label><select class="input-small" ng-model="dashboard.style" ng-options="f for f in ['dark','light']" ng-change="styleUpdated()"></select>
</div>
<div class="editor-option"> <div class="editor-option">
<label class="small">Time correction</label> <label class="small">Time correction</label>
<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select> <select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>

View File

@ -7,7 +7,12 @@
<title>Grafana</title> <title>Grafana</title>
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.dark.min.css" title="Dark"> [[if .User.LightTheme]]
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.light.min.css">
[[else]]
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.dark.min.css">
[[end]]
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/img/fav32.png"> <link rel="icon" type="image/png" href="[[.AppSubUrl]]/img/fav32.png">
<base href="[[.AppSubUrl]]/" /> <base href="[[.AppSubUrl]]/" />
@ -19,14 +24,9 @@
<script src="[[.AppSubUrl]]/public/vendor/require/require.js"></script> <script src="[[.AppSubUrl]]/public/vendor/require/require.js"></script>
<script src="[[.AppSubUrl]]/public/app/components/require.backend.js"></script> <script src="[[.AppSubUrl]]/public/app/components/require.backend.js"></script>
<!-- endbuild --> <!-- endbuild -->
</head> </head>
<body ng-cloak ng-controller="GrafanaCtrl" ng-class="{'sidemenu-open': contextSrv.sidemenu}"> <body ng-cloak ng-controller="GrafanaCtrl" ng-class="{'sidemenu-open': contextSrv.sidemenu}">
<link rel="stylesheet" href="[[.AppSubUrl]]/css/grafana.light.min.css" ng-if="contextSrv.lightTheme">
<div class="sidemenu-canvas"> <div class="sidemenu-canvas">
<aside class="sidemenu-wrapper" ng-if="contextSrv.sidemenu"> <aside class="sidemenu-wrapper" ng-if="contextSrv.sidemenu">