Просмотр исходного кода

Merge branch 'develop' into switch-slider-test

Torkel Ödegaard 7 лет назад
Родитель
Сommit
808a0aa6f0
59 измененных файлов с 952 добавлено и 774 удалено
  1. 11 4
      CHANGELOG.md
  2. 65 1
      docs/sources/http_api/team.md
  3. 7 6
      package.json
  4. 3 1
      pkg/api/api.go
  5. 1 1
      pkg/api/dashboard.go
  6. 4 4
      pkg/api/dtos/plugins.go
  7. 1 1
      pkg/api/frontendsettings.go
  8. 1 1
      pkg/api/index.go
  9. 5 1
      pkg/api/plugins.go
  10. 8 7
      pkg/api/preferences.go
  11. 10 0
      pkg/api/team.go
  12. 4 3
      pkg/models/preferences.go
  13. 8 1
      pkg/plugins/models.go
  14. 9 0
      pkg/services/sqlstore/migrations/preferences_mig.go
  15. 19 7
      pkg/services/sqlstore/preferences.go
  16. 91 0
      pkg/services/sqlstore/preferences_test.go
  17. 0 28
      public/app/core/actions/user.ts
  18. 4 1
      public/app/core/components/Picker/SimplePicker.tsx
  19. 141 0
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  20. 11 26
      public/app/core/components/Tooltip/Popover.tsx
  21. 69 0
      public/app/core/components/Tooltip/Popper.tsx
  22. 9 28
      public/app/core/components/Tooltip/Tooltip.tsx
  23. 2 2
      public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap
  24. 3 3
      public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap
  25. 89 0
      public/app/core/components/Tooltip/withPopper.tsx
  26. 0 58
      public/app/core/components/Tooltip/withTooltip.tsx
  27. 0 2
      public/app/core/reducers/index.ts
  28. 0 15
      public/app/core/reducers/user.ts
  29. 26 29
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  30. 89 0
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx
  31. 2 0
      public/app/features/dashboard/panel_model.ts
  32. 12 0
      public/app/features/explore/Explore.tsx
  33. 1 4
      public/app/features/org/OrgDetailsPage.test.tsx
  34. 8 22
      public/app/features/org/OrgDetailsPage.tsx
  35. 0 28
      public/app/features/org/OrgPreferences.test.tsx
  36. 0 113
      public/app/features/org/OrgPreferences.tsx
  37. 3 1
      public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap
  38. 0 136
      public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap
  39. 3 70
      public/app/features/org/state/actions.ts
  40. 1 14
      public/app/features/org/state/reducers.ts
  41. 3 91
      public/app/features/profile/PrefControlCtrl.ts
  42. 1 1
      public/app/features/profile/partials/profile.html
  43. 6 1
      public/app/features/teams/TeamPages.test.tsx
  44. 3 4
      public/app/features/teams/TeamPages.tsx
  45. 6 2
      public/app/features/teams/TeamSettings.tsx
  46. 1 1
      public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
  47. 3 0
      public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap
  48. 6 5
      public/app/features/teams/state/selectors.test.ts
  49. 8 4
      public/app/plugins/datasource/elasticsearch/config_ctrl.ts
  50. 6 0
      public/app/plugins/datasource/elasticsearch/metric_agg.ts
  51. 2 2
      public/app/plugins/datasource/elasticsearch/query_builder.ts
  52. 13 0
      public/app/plugins/datasource/elasticsearch/query_ctrl.ts
  53. 8 0
      public/app/plugins/datasource/elasticsearch/query_def.ts
  54. 1 2
      public/app/types/index.ts
  55. 0 7
      public/app/types/organization.ts
  56. 18 10
      public/sass/components/_popper.scss
  57. 13 1
      public/sass/pages/_explore.scss
  58. 4 0
      public/sass/utils/_utils.scss
  59. 130 25
      yarn.lock

+ 11 - 4
CHANGELOG.md

@@ -8,24 +8,31 @@
 * **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
 * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
 * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
+* **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550)
 
 ### Minor
 
 * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
 * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
+* **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367)
+* **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977)
+* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
 * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
-* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
-* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
-* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
 * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
-* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
 * **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov)
+* **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
+* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
 
 ### Breaking changes
 
 * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
 
+# 5.3.5 (unreleased)
+
+* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
+
 # 5.3.4 (2018-11-13)
 
 * **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)

+ 65 - 1
docs/sources/http_api/team.md

@@ -30,7 +30,7 @@ Authorization: Basic YWRtaW46YWRtaW4=
 
 ### Using the query parameter
 
-Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. 
+Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`.
 
 The `totalCount` field in the response can be used for pagination of the teams list E.g. if `totalCount` is equal to 100 teams and the `perpage` parameter is set to 10 then there are 10 pages of teams.
 
@@ -314,3 +314,67 @@ Status Codes:
 - **401** - Unauthorized
 - **403** - Permission denied
 - **404** - Team not found/Team member not found
+
+## Get Team Preferences
+
+`GET /api/teams/:teamId/preferences`
+
+**Example Request**:
+
+```http
+GET /api/teams/2/preferences HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+  "theme": "",
+  "homeDashboardId": 0,
+  "timezone": ""
+}
+```
+
+## Update Team Preferences
+
+`PUT /api/teams/:teamId/preferences`
+
+**Example Request**:
+
+```http
+PUT /api/teams/2/preferences HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+  "theme": "dark",
+  "homeDashboardId": 39,
+  "timezone": "utc"
+}
+```
+
+JSON Body Schema:
+
+- **theme** - One of: ``light``, ``dark``, or an empty string for the default theme
+- **homeDashboardId** - The numerical ``:id`` of a dashboard, default: ``0``
+- **timezone** - One of: ``utc``, ``browser``, or an empty string for the default
+
+Omitting a key will cause the current value to be replaced with the system default value.
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: text/plain; charset=utf-8
+
+{
+  "message":"Preferences updated"
+}
+```

+ 7 - 6
package.json

@@ -14,9 +14,9 @@
     "@types/enzyme": "^3.1.13",
     "@types/jest": "^23.3.2",
     "@types/node": "^8.0.31",
-    "@types/react": "^16.4.14",
+    "@types/react": "^16.7.6",
     "@types/react-custom-scrollbars": "^4.0.5",
-    "@types/react-dom": "^16.0.7",
+    "@types/react-dom": "^16.0.9",
     "@types/react-select": "^2.0.4",
     "angular-mocks": "1.6.6",
     "autoprefixer": "^6.4.0",
@@ -152,12 +152,12 @@
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.2",
     "rc-cascader": "^0.14.0",
-    "react": "^16.5.0",
+    "react": "^16.6.3",
     "react-custom-scrollbars": "^4.2.1",
-    "react-dom": "^16.5.0",
+    "react-dom": "^16.6.3",
     "react-grid-layout": "0.16.6",
     "react-highlight-words": "^0.10.0",
-    "react-popper": "^0.7.5",
+    "react-popper": "^1.3.0",
     "react-redux": "^5.0.7",
     "react-select": "2.1.0",
     "react-sizeme": "^2.3.6",
@@ -180,6 +180,7 @@
     "tslint-react": "^3.6.0"
   },
   "resolutions": {
-    "caniuse-db": "1.0.30000772"
+    "caniuse-db": "1.0.30000772",
+    "**/@types/react": "16.7.6"
   }
 }

+ 3 - 1
pkg/api/api.go

@@ -155,6 +155,8 @@ func (hs *HTTPServer) registerRoutes() {
 			teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
 			teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
 			teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
+			teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences))
+			teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences))
 		}, reqOrgAdmin)
 
 		// team without requirement of user to be org admin
@@ -242,7 +244,7 @@ func (hs *HTTPServer) registerRoutes() {
 
 		apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
 
-		apiRoute.Get("/plugins", Wrap(GetPluginList))
+		apiRoute.Get("/plugins", Wrap(hs.GetPluginList))
 		apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
 		apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown))
 

+ 1 - 1
pkg/api/dashboard.go

@@ -293,7 +293,7 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 }
 
 func GetHomeDashboard(c *m.ReqContext) Response {
-	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
+	prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 		return Error(500, "Failed to get preferences", err)
 	}

+ 4 - 4
pkg/api/dtos/plugins.go

@@ -19,9 +19,9 @@ type PluginSetting struct {
 	JsonData      map[string]interface{}      `json:"jsonData"`
 	DefaultNavUrl string                      `json:"defaultNavUrl"`
 
-	LatestVersion string `json:"latestVersion"`
-	HasUpdate     bool   `json:"hasUpdate"`
-	State         string `json:"state"`
+	LatestVersion string              `json:"latestVersion"`
+	HasUpdate     bool                `json:"hasUpdate"`
+	State         plugins.PluginState `json:"state"`
 }
 
 type PluginListItem struct {
@@ -34,7 +34,7 @@ type PluginListItem struct {
 	LatestVersion string              `json:"latestVersion"`
 	HasUpdate     bool                `json:"hasUpdate"`
 	DefaultNavUrl string              `json:"defaultNavUrl"`
-	State         string              `json:"state"`
+	State         plugins.PluginState `json:"state"`
 }
 
 type PluginList []PluginListItem

+ 1 - 1
pkg/api/frontendsettings.go

@@ -133,7 +133,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
 
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
-		if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
+		if panel.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels {
 			continue
 		}
 

+ 1 - 1
pkg/api/index.go

@@ -23,7 +23,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
 		return nil, err
 	}
 
-	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
+	prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 		return nil, err
 	}

+ 5 - 1
pkg/api/plugins.go

@@ -10,7 +10,7 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-func GetPluginList(c *m.ReqContext) Response {
+func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
 	typeFilter := c.Query("type")
 	enabledFilter := c.Query("enabled")
 	embeddedFilter := c.Query("embedded")
@@ -39,6 +39,10 @@ func GetPluginList(c *m.ReqContext) Response {
 			continue
 		}
 
+		if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels {
+			continue
+		}
+
 		listItem := dtos.PluginListItem{
 			Id:            pluginDef.Id,
 			Name:          pluginDef.Name,

+ 8 - 7
pkg/api/preferences.go

@@ -21,11 +21,11 @@ func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response {
 
 // GET /api/user/preferences
 func GetUserPreferences(c *m.ReqContext) Response {
-	return getPreferencesFor(c.OrgId, c.UserId)
+	return getPreferencesFor(c.OrgId, c.UserId, 0)
 }
 
-func getPreferencesFor(orgID int64, userID int64) Response {
-	prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID}
+func getPreferencesFor(orgID, userID, teamID int64) Response {
+	prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID, TeamId: teamID}
 
 	if err := bus.Dispatch(&prefsQuery); err != nil {
 		return Error(500, "Failed to get preferences", err)
@@ -42,13 +42,14 @@ func getPreferencesFor(orgID int64, userID int64) Response {
 
 // PUT /api/user/preferences
 func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
-	return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
+	return updatePreferencesFor(c.OrgId, c.UserId, 0, &dtoCmd)
 }
 
-func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
+func updatePreferencesFor(orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
 	saveCmd := m.SavePreferencesCommand{
 		UserId:          userID,
 		OrgId:           orgID,
+		TeamId:          teamId,
 		Theme:           dtoCmd.Theme,
 		Timezone:        dtoCmd.Timezone,
 		HomeDashboardId: dtoCmd.HomeDashboardID,
@@ -63,10 +64,10 @@ func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd
 
 // GET /api/org/preferences
 func GetOrgPreferences(c *m.ReqContext) Response {
-	return getPreferencesFor(c.OrgId, 0)
+	return getPreferencesFor(c.OrgId, 0, 0)
 }
 
 // PUT /api/org/preferences
 func UpdateOrgPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
-	return updatePreferencesFor(c.OrgId, 0, &dtoCmd)
+	return updatePreferencesFor(c.OrgId, 0, 0, &dtoCmd)
 }

+ 10 - 0
pkg/api/team.go

@@ -96,3 +96,13 @@ func GetTeamByID(c *m.ReqContext) Response {
 	query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name)
 	return JSON(200, &query.Result)
 }
+
+// GET /api/teams/:teamId/preferences
+func GetTeamPreferences(c *m.ReqContext) Response {
+	return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"))
+}
+
+// PUT /api/teams/:teamId/preferences
+func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response {
+	return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd)
+}

+ 4 - 3
pkg/models/preferences.go

@@ -14,6 +14,7 @@ type Preferences struct {
 	Id              int64
 	OrgId           int64
 	UserId          int64
+	TeamId          int64
 	Version         int
 	HomeDashboardId int64
 	Timezone        string
@@ -29,14 +30,13 @@ type GetPreferencesQuery struct {
 	Id     int64
 	OrgId  int64
 	UserId int64
+	TeamId int64
 
 	Result *Preferences
 }
 
 type GetPreferencesWithDefaultsQuery struct {
-	Id     int64
-	OrgId  int64
-	UserId int64
+	User *SignedInUser
 
 	Result *Preferences
 }
@@ -46,6 +46,7 @@ type GetPreferencesWithDefaultsQuery struct {
 type SavePreferencesCommand struct {
 	UserId int64
 	OrgId  int64
+	TeamId int64
 
 	HomeDashboardId int64  `json:"homeDashboardId"`
 	Timezone        string `json:"timezone"`

+ 8 - 1
pkg/plugins/models.go

@@ -17,6 +17,13 @@ var (
 	PluginTypeDashboard  = "dashboard"
 )
 
+type PluginState string
+
+var (
+	PluginStateAlpha PluginState = "alpha"
+	PluginStateBeta  PluginState = "beta"
+)
+
 type PluginNotFoundError struct {
 	PluginId string
 }
@@ -39,7 +46,7 @@ type PluginBase struct {
 	Module       string             `json:"module"`
 	BaseUrl      string             `json:"baseUrl"`
 	HideFromList bool               `json:"hideFromList,omitempty"`
-	State        string             `json:"state,omitempty"`
+	State        PluginState        `json:"state,omitempty"`
 
 	IncludedInAppId string `json:"-"`
 	PluginDir       string `json:"-"`

+ 9 - 0
pkg/services/sqlstore/migrations/preferences_mig.go

@@ -34,4 +34,13 @@ func addPreferencesMigrations(mg *Migrator) {
 		{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false},
 		{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false},
 	}))
+
+	mg.AddMigration("Add column team_id in preferences", NewAddColumnMigration(preferencesV2, &Column{
+		Name: "team_id", Type: DB_BigInt, Nullable: true,
+	}))
+
+	mg.AddMigration("Update team_id column values in preferences", NewRawSqlMigration("").
+		Sqlite("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;").
+		Postgres("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;").
+		Mysql("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;"))
 }

+ 19 - 7
pkg/services/sqlstore/preferences.go

@@ -1,6 +1,7 @@
 package sqlstore
 
 import (
+	"strings"
 	"time"
 
 	"github.com/grafana/grafana/pkg/bus"
@@ -16,11 +17,22 @@ func init() {
 }
 
 func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error {
-
+	params := make([]interface{}, 0)
+	filter := ""
+	if len(query.User.Teams) > 0 {
+		filter = "(org_id=? AND team_id IN (?" + strings.Repeat(",?", len(query.User.Teams)-1) + ")) OR "
+		params = append(params, query.User.OrgId)
+		for _, v := range query.User.Teams {
+			params = append(params, v)
+		}
+	}
+	filter += "(org_id=? AND user_id=? AND team_id=0) OR (org_id=? AND team_id=0 AND user_id=0)"
+	params = append(params, query.User.OrgId)
+	params = append(params, query.User.UserId)
+	params = append(params, query.User.OrgId)
 	prefs := make([]*m.Preferences, 0)
-	filter := "(org_id=? AND user_id=?) OR (org_id=? AND user_id=0)"
-	err := x.Where(filter, query.OrgId, query.UserId, query.OrgId).
-		OrderBy("user_id ASC").
+	err := x.Where(filter, params...).
+		OrderBy("user_id ASC, team_id ASC").
 		Find(&prefs)
 
 	if err != nil {
@@ -50,9 +62,8 @@ func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error
 }
 
 func GetPreferences(query *m.GetPreferencesQuery) error {
-
 	var prefs m.Preferences
-	exists, err := x.Where("org_id=? AND user_id=?", query.OrgId, query.UserId).Get(&prefs)
+	exists, err := x.Where("org_id=? AND user_id=? AND team_id=?", query.OrgId, query.UserId, query.TeamId).Get(&prefs)
 
 	if err != nil {
 		return err
@@ -71,7 +82,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 
 		var prefs m.Preferences
-		exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
+		exists, err := sess.Where("org_id=? AND user_id=? AND team_id=?", cmd.OrgId, cmd.UserId, cmd.TeamId).Get(&prefs)
 		if err != nil {
 			return err
 		}
@@ -80,6 +91,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
 			prefs = m.Preferences{
 				UserId:          cmd.UserId,
 				OrgId:           cmd.OrgId,
+				TeamId:          cmd.TeamId,
 				HomeDashboardId: cmd.HomeDashboardId,
 				Timezone:        cmd.Timezone,
 				Theme:           cmd.Theme,

+ 91 - 0
pkg/services/sqlstore/preferences_test.go

@@ -0,0 +1,91 @@
+package sqlstore
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+func TestPreferencesDataAccess(t *testing.T) {
+	Convey("Testing preferences data access", t, func() {
+		InitTestDB(t)
+
+		Convey("GetPreferencesWithDefaults with no saved preferences should return defaults", func() {
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.Theme, ShouldEqual, setting.DefaultTheme)
+			So(query.Result.Timezone, ShouldEqual, "browser")
+			So(query.Result.HomeDashboardId, ShouldEqual, 0)
+		})
+
+		Convey("GetPreferencesWithDefaults with saved org and user home dashboard should return user home dashboard", func() {
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.HomeDashboardId, ShouldEqual, 4)
+		})
+
+		Convey("GetPreferencesWithDefaults with saved org and other user home dashboard should return org home dashboard", func() {
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.HomeDashboardId, ShouldEqual, 1)
+		})
+
+		Convey("GetPreferencesWithDefaults with saved org and teams home dashboard should return last team home dashboard", func() {
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, Teams: []int64{2, 3}}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.HomeDashboardId, ShouldEqual, 3)
+		})
+
+		Convey("GetPreferencesWithDefaults with saved org and other teams home dashboard should return org home dashboard", func() {
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.HomeDashboardId, ShouldEqual, 1)
+		})
+
+		Convey("GetPreferencesWithDefaults with saved org, teams and user home dashboard should return user home dashboard", func() {
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1, Teams: []int64{2, 3}}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.HomeDashboardId, ShouldEqual, 4)
+		})
+
+		Convey("GetPreferencesWithDefaults with saved org, other teams and user home dashboard should return org home dashboard", func() {
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3})
+			SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4})
+
+			query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}}
+			err := GetPreferencesWithDefaults(query)
+			So(err, ShouldBeNil)
+			So(query.Result.HomeDashboardId, ShouldEqual, 1)
+		})
+	})
+}

+ 0 - 28
public/app/core/actions/user.ts

@@ -1,28 +0,0 @@
-import { ThunkAction } from 'redux-thunk';
-import { getBackendSrv } from '../services/backend_srv';
-import { DashboardAcl, DashboardSearchHit, StoreState } from '../../types';
-
-type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
-
-export type Action = LoadStarredDashboardsAction;
-
-export enum ActionTypes {
-  LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS',
-}
-
-interface LoadStarredDashboardsAction {
-  type: ActionTypes.LoadStarredDashboards;
-  payload: DashboardSearchHit[];
-}
-
-const starredDashboardsLoaded = (dashboards: DashboardAcl[]) => ({
-  type: ActionTypes.LoadStarredDashboards,
-  payload: dashboards,
-});
-
-export function loadStarredDashboards(): ThunkResult<void> {
-  return async dispatch => {
-    const starredDashboards = await getBackendSrv().search({ starred: true });
-    dispatch(starredDashboardsLoaded(starredDashboards));
-  };
-}

+ 4 - 1
public/app/core/components/Picker/SimplePicker.tsx

@@ -5,13 +5,14 @@ import ResetStyles from './ResetStyles';
 
 interface Props {
   className?: string;
-  defaultValue: any;
+  defaultValue?: any;
   getOptionLabel: (item: any) => string;
   getOptionValue: (item: any) => string;
   onSelected: (item: any) => {} | void;
   options: any[];
   placeholder?: string;
   width: number;
+  value: any;
 }
 
 const SimplePicker: SFC<Props> = ({
@@ -23,6 +24,7 @@ const SimplePicker: SFC<Props> = ({
   options,
   placeholder,
   width,
+  value,
 }) => {
   return (
     <Select
@@ -32,6 +34,7 @@ const SimplePicker: SFC<Props> = ({
         Option: DescriptionOption,
       }}
       defaultValue={defaultValue}
+      value={value}
       getOptionLabel={getOptionLabel}
       getOptionValue={getOptionValue}
       isSearchable={false}

+ 141 - 0
public/app/core/components/SharedPreferences/SharedPreferences.tsx

@@ -0,0 +1,141 @@
+import React, { PureComponent } from 'react';
+
+import { Label } from 'app/core/components/Label/Label';
+import SimplePicker from 'app/core/components/Picker/SimplePicker';
+import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
+
+import { DashboardSearchHit } from 'app/types';
+
+export interface Props {
+  resourceUri: string;
+}
+
+export interface State {
+  homeDashboardId: number;
+  theme: string;
+  timezone: string;
+  dashboards: DashboardSearchHit[];
+}
+
+const themes = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
+
+const timezones = [
+  { value: '', text: 'Default' },
+  { value: 'browser', text: 'Local browser time' },
+  { value: 'utc', text: 'UTC' },
+];
+
+export class SharedPreferences extends PureComponent<Props, State> {
+  backendSrv: BackendSrv = getBackendSrv();
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      homeDashboardId: 0,
+      theme: '',
+      timezone: '',
+      dashboards: [],
+    };
+  }
+
+  async componentDidMount() {
+    const prefs = await this.backendSrv.get(`/api/${this.props.resourceUri}/preferences`);
+    const dashboards = await this.backendSrv.search({ starred: true });
+
+    if (prefs.homeDashboardId > 0 && !dashboards.find(d => d.id === prefs.homeDashboardId)) {
+      const missing = await this.backendSrv.search({ dashboardIds: [prefs.homeDashboardId] });
+      if (missing && missing.length > 0) {
+        dashboards.push(missing[0]);
+      }
+    }
+
+    this.setState({
+      homeDashboardId: prefs.homeDashboardId,
+      theme: prefs.theme,
+      timezone: prefs.timezone,
+      dashboards: [{ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' }, ...dashboards],
+    });
+  }
+
+  onSubmitForm = async event => {
+    event.preventDefault();
+
+    const { homeDashboardId, theme, timezone } = this.state;
+
+    await this.backendSrv.put(`/api/${this.props.resourceUri}/preferences`, {
+      homeDashboardId,
+      theme,
+      timezone,
+    });
+    window.location.reload();
+  };
+
+  onThemeChanged = (theme: string) => {
+    this.setState({ theme });
+  };
+
+  onTimeZoneChanged = (timezone: string) => {
+    this.setState({ timezone });
+  };
+
+  onHomeDashboardChanged = (dashboardId: number) => {
+    this.setState({ homeDashboardId: dashboardId });
+  };
+
+  render() {
+    const { theme, timezone, homeDashboardId, dashboards } = this.state;
+
+    return (
+      <form className="section gf-form-group" onSubmit={this.onSubmitForm}>
+        <h3 className="page-heading">Preferences</h3>
+        <div className="gf-form">
+          <span className="gf-form-label width-11">UI Theme</span>
+          <SimplePicker
+            value={themes.find(item => item.value === theme)}
+            options={themes}
+            getOptionValue={i => i.value}
+            getOptionLabel={i => i.text}
+            onSelected={theme => this.onThemeChanged(theme.value)}
+            width={20}
+          />
+        </div>
+        <div className="gf-form">
+          <Label
+            width={11}
+            tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
+          >
+            Home Dashboard
+          </Label>
+          <SimplePicker
+            value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
+            getOptionValue={i => i.id}
+            getOptionLabel={i => i.title}
+            onSelected={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
+            options={dashboards}
+            placeholder="Chose default dashboard"
+            width={20}
+          />
+        </div>
+        <div className="gf-form">
+          <label className="gf-form-label width-11">Timezone</label>
+          <SimplePicker
+            value={timezones.find(item => item.value === timezone)}
+            getOptionValue={i => i.value}
+            getOptionLabel={i => i.text}
+            onSelected={timezone => this.onTimeZoneChanged(timezone.value)}
+            options={timezones}
+            width={20}
+          />
+        </div>
+        <div className="gf-form-button-row">
+          <button type="submit" className="btn btn-success">
+            Save
+          </button>
+        </div>
+      </form>
+    );
+  }
+}
+
+export default SharedPreferences;

+ 11 - 26
public/app/core/components/Tooltip/Popover.tsx

@@ -1,34 +1,19 @@
-import React from 'react';
-import withTooltip from './withTooltip';
-import { Target } from 'react-popper';
+import React, { PureComponent } from 'react';
+import Popper from './Popper';
+import withPopper, { UsingPopperProps } from './withPopper';
 
-interface PopoverProps {
-  tooltipSetState: (prevState: object) => void;
-}
-
-class Popover extends React.Component<PopoverProps, any> {
-  constructor(props) {
-    super(props);
-    this.toggleTooltip = this.toggleTooltip.bind(this);
-  }
+class Popover extends PureComponent<UsingPopperProps> {
+  render() {
+    const { children, hidePopper, showPopper, className, ...restProps } = this.props;
 
-  toggleTooltip() {
-    const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => {
-      return {
-        ...prevState,
-        show: !prevState.show,
-      };
-    });
-  }
+    const togglePopper = restProps.show ? hidePopper : showPopper;
 
-  render() {
     return (
-      <Target className="popper__target" onClick={this.toggleTooltip}>
-        {this.props.children}
-      </Target>
+      <div className={`popper__manager ${className}`} onClick={togglePopper}>
+        <Popper {...restProps}>{children}</Popper>
+      </div>
     );
   }
 }
 
-export default withTooltip(Popover);
+export default withPopper(Popover);

+ 69 - 0
public/app/core/components/Tooltip/Popper.tsx

@@ -0,0 +1,69 @@
+import React, { PureComponent } from 'react';
+import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
+import Transition from 'react-transition-group/Transition';
+
+const defaultTransitionStyles = {
+  transition: 'opacity 200ms linear',
+  opacity: 0,
+};
+
+const transitionStyles = {
+  exited: { opacity: 0 },
+  entering: { opacity: 0 },
+  entered: { opacity: 1 },
+  exiting: { opacity: 0 },
+};
+
+interface Props {
+  renderContent: (content: any) => any;
+  show: boolean;
+  placement?: any;
+  content: string | ((props: any) => JSX.Element);
+  refClassName?: string;
+}
+
+class Popper extends PureComponent<Props> {
+  render() {
+    const { children, renderContent, show, placement, refClassName } = this.props;
+    const { content } = this.props;
+
+    return (
+      <Manager>
+        <Reference>
+          {({ ref }) => (
+            <div className={`popper_ref ${refClassName || ''}`} ref={ref}>
+              {children}
+            </div>
+          )}
+        </Reference>
+        <Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
+          {transitionState => (
+            <ReactPopper placement={placement}>
+              {({ ref, style, placement, arrowProps }) => {
+                return (
+                  <div
+                    ref={ref}
+                    style={{
+                      ...style,
+                      ...defaultTransitionStyles,
+                      ...transitionStyles[transitionState],
+                    }}
+                    data-placement={placement}
+                    className="popper"
+                  >
+                    <div className="popper__background">
+                      {renderContent(content)}
+                      <div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
+                    </div>
+                  </div>
+                );
+              }}
+            </ReactPopper>
+          )}
+        </Transition>
+      </Manager>
+    );
+  }
+}
+
+export default Popper;

+ 9 - 28
public/app/core/components/Tooltip/Tooltip.tsx

@@ -1,36 +1,17 @@
 import React, { PureComponent } from 'react';
-import withTooltip from './withTooltip';
-import { Target } from 'react-popper';
-
-interface Props {
-  tooltipSetState: (prevState: object) => void;
-}
-
-class Tooltip extends PureComponent<Props> {
-  showTooltip = () => {
-    const { tooltipSetState } = this.props;
-
-    tooltipSetState(prevState => ({
-      ...prevState,
-      show: true,
-    }));
-  };
-
-  hideTooltip = () => {
-    const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => ({
-      ...prevState,
-      show: false,
-    }));
-  };
+import Popper from './Popper';
+import withPopper, { UsingPopperProps } from './withPopper';
 
+class Tooltip extends PureComponent<UsingPopperProps> {
   render() {
+    const { children, hidePopper, showPopper, className, ...restProps } = this.props;
+
     return (
-      <Target className="popper__target" onMouseOver={this.showTooltip} onMouseOut={this.hideTooltip}>
-        {this.props.children}
-      </Target>
+      <div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
+        <Popper {...restProps}>{children}</Popper>
+      </div>
     );
   }
 }
 
-export default withTooltip(Tooltip);
+export default withPopper(Tooltip);

+ 2 - 2
public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap

@@ -3,10 +3,10 @@
 exports[`Popover renders correctly 1`] = `
 <div
   className="popper__manager test-class"
+  onClick={[Function]}
 >
   <div
-    className="popper__target"
-    onClick={[Function]}
+    className="popper_ref "
   >
     <button>
       Button with Popover

+ 3 - 3
public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap

@@ -3,11 +3,11 @@
 exports[`Tooltip renders correctly 1`] = `
 <div
   className="popper__manager test-class"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
 >
   <div
-    className="popper__target"
-    onMouseOut={[Function]}
-    onMouseOver={[Function]}
+    className="popper_ref "
   >
     <a
       href="http://www.grafana.com"

+ 89 - 0
public/app/core/components/Tooltip/withPopper.tsx

@@ -0,0 +1,89 @@
+import React from 'react';
+
+export interface UsingPopperProps {
+  showPopper: (prevState: object) => void;
+  hidePopper: (prevState: object) => void;
+  renderContent: (content: any) => any;
+  show: boolean;
+  placement?: string;
+  content: string | ((props: any) => JSX.Element);
+  className?: string;
+  refClassName?: string;
+}
+
+interface Props {
+  placement?: string;
+  className?: string;
+  refClassName?: string;
+  content: string | ((props: any) => JSX.Element);
+}
+
+interface State {
+  placement: string;
+  show: boolean;
+}
+
+export default function withPopper(WrappedComponent) {
+  return class extends React.Component<Props, State> {
+    constructor(props) {
+      super(props);
+      this.setState = this.setState.bind(this);
+      this.state = {
+        placement: this.props.placement || 'auto',
+        show: false,
+      };
+    }
+
+    componentWillReceiveProps(nextProps) {
+      if (nextProps.placement && nextProps.placement !== this.state.placement) {
+        this.setState(prevState => {
+          return {
+            ...prevState,
+            placement: nextProps.placement,
+          };
+        });
+      }
+    }
+
+    showPopper = () => {
+      this.setState(prevState => ({
+        ...prevState,
+        show: true,
+      }));
+    };
+
+    hidePopper = () => {
+      this.setState(prevState => ({
+        ...prevState,
+        show: false,
+      }));
+    };
+
+    renderContent(content) {
+      console.log('render content');
+      if (typeof content === 'function') {
+        // If it's a function we assume it's a React component
+        const ReactComponent = content;
+        return <ReactComponent />;
+      }
+      return content;
+    }
+
+    render() {
+      const { show, placement } = this.state;
+      const className = this.props.className || '';
+
+      return (
+        <WrappedComponent
+          {...this.props}
+          showPopper={this.showPopper}
+          hidePopper={this.hidePopper}
+          renderContent={this.renderContent}
+          show={show}
+          placement={placement}
+          className={className}
+        />
+      );
+    }
+  };
+}

+ 0 - 58
public/app/core/components/Tooltip/withTooltip.tsx

@@ -1,58 +0,0 @@
-import React from 'react';
-import { Manager, Popper, Arrow } from 'react-popper';
-
-interface IwithTooltipProps {
-  placement?: string;
-  content: string | ((props: any) => JSX.Element);
-  className?: string;
-}
-
-export default function withTooltip(WrappedComponent) {
-  return class extends React.Component<IwithTooltipProps, any> {
-    constructor(props) {
-      super(props);
-
-      this.setState = this.setState.bind(this);
-      this.state = {
-        placement: this.props.placement || 'auto',
-        show: false,
-      };
-    }
-
-    componentWillReceiveProps(nextProps) {
-      if (nextProps.placement && nextProps.placement !== this.state.placement) {
-        this.setState(prevState => {
-          return {
-            ...prevState,
-            placement: nextProps.placement,
-          };
-        });
-      }
-    }
-
-    renderContent(content) {
-      if (typeof content === 'function') {
-        // If it's a function we assume it's a React component
-        const ReactComponent = content;
-        return <ReactComponent />;
-      }
-      return content;
-    }
-
-    render() {
-      const { content, className } = this.props;
-
-      return (
-        <Manager className={`popper__manager ${className || ''}`}>
-          <WrappedComponent {...this.props} tooltipSetState={this.setState} />
-          {this.state.show ? (
-            <Popper placement={this.state.placement} className="popper">
-              {this.renderContent(content)}
-              <Arrow className="popper__arrow" />
-            </Popper>
-          ) : null}
-        </Manager>
-      );
-    }
-  };
-}

+ 0 - 2
public/app/core/reducers/index.ts

@@ -1,11 +1,9 @@
 import { navIndexReducer as navIndex } from './navModel';
 import { locationReducer as location } from './location';
 import { appNotificationsReducer as appNotifications } from './appNotification';
-import { userReducer as user } from './user';
 
 export default {
   navIndex,
   location,
   appNotifications,
-  user,
 };

+ 0 - 15
public/app/core/reducers/user.ts

@@ -1,15 +0,0 @@
-import { DashboardSearchHit, UserState } from '../../types';
-import { Action, ActionTypes } from '../actions/user';
-
-const initialState: UserState = {
-  starredDashboards: [] as DashboardSearchHit[],
-};
-
-export const userReducer = (state: UserState = initialState, action: Action): UserState => {
-  switch (action.type) {
-    case ActionTypes.LoadStarredDashboards:
-      return { ...state, starredDashboards: action.payload };
-  }
-
-  return state;
-};

+ 26 - 29
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -1,6 +1,7 @@
 import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 
+import PanelHeaderCorner from './PanelHeaderCorner';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
 
 import { DashboardModel } from 'app/features/dashboard/dashboard_model';
@@ -41,41 +42,37 @@ export class PanelHeader extends PureComponent<Props, State> {
     const isLoading = false;
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
     const { panel, dashboard, timeInfo } = this.props;
-
     return (
-      <div className={panelHeaderClass}>
-        <span className="panel-info-corner">
-          <i className="fa" />
-          <span className="panel-info-corner-inner" />
-        </span>
-
-        {isLoading && (
-          <span className="panel-loading">
-            <i className="fa fa-spinner fa-spin" />
-          </span>
-        )}
-
-        <div className="panel-title-container" onClick={this.onMenuToggle}>
-          <div className="panel-title">
-            <span className="icon-gf panel-alert-icon" />
-            <span className="panel-title-text">
-              {panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
+      <>
+        <PanelHeaderCorner panel={panel} />
+        <div className={panelHeaderClass}>
+          {isLoading && (
+            <span className="panel-loading">
+              <i className="fa fa-spinner fa-spin" />
             </span>
+          )}
+          <div className="panel-title-container" onClick={this.onMenuToggle}>
+            <div className="panel-title">
+              <span className="icon-gf panel-alert-icon" />
+              <span className="panel-title-text">
+                {panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
+              </span>
 
-            {this.state.panelMenuOpen && (
-              <ClickOutsideWrapper onClick={this.closeMenu}>
-                <PanelHeaderMenu panel={panel} dashboard={dashboard} />
-              </ClickOutsideWrapper>
-            )}
+              {this.state.panelMenuOpen && (
+                <ClickOutsideWrapper onClick={this.closeMenu}>
+                  <PanelHeaderMenu panel={panel} dashboard={dashboard} />
+                </ClickOutsideWrapper>
+              )}
 
-            {timeInfo && (
-              <span className="panel-time-info">
-                <i className="fa fa-clock-o" /> {timeInfo}
-              </span>
-            )}
+              {timeInfo && (
+                <span className="panel-time-info">
+                  <i className="fa fa-clock-o" /> {timeInfo}
+                </span>
+              )}
+            </div>
           </div>
         </div>
-      </div>
+      </>
     );
   }
 }

+ 89 - 0
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx

@@ -0,0 +1,89 @@
+import React, { PureComponent } from 'react';
+import { PanelModel } from 'app/features/dashboard/panel_model';
+import Tooltip from 'app/core/components/Tooltip/Tooltip';
+import templateSrv from 'app/features/templating/template_srv';
+import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv';
+import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv';
+import Remarkable from 'remarkable';
+
+enum InfoModes {
+  Error = 'Error',
+  Info = 'Info',
+  Links = 'Links',
+}
+
+interface Props {
+  panel: PanelModel;
+}
+
+export class PanelHeaderCorner extends PureComponent<Props> {
+  timeSrv: TimeSrv = getTimeSrv();
+
+  getInfoMode = () => {
+    const { panel } = this.props;
+    if (!!panel.description) {
+      return InfoModes.Info;
+    }
+    if (panel.links && panel.links.length) {
+      return InfoModes.Links;
+    }
+
+    return undefined;
+  };
+
+  getInfoContent = (): JSX.Element => {
+    const { panel } = this.props;
+    const markdown = panel.description;
+    const linkSrv = new LinkSrv(templateSrv, this.timeSrv);
+    const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
+    const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown);
+
+    const html = (
+      <div className="markdown-html">
+        <div dangerouslySetInnerHTML={{ __html: remarkableInterpolatedMarkdown }} />
+        {panel.links &&
+          panel.links.length > 0 && (
+            <ul className="text-left">
+              {panel.links.map((link, idx) => {
+                const info = linkSrv.getPanelLinkAnchorInfo(link, panel.scopedVars);
+                return (
+                  <li key={idx}>
+                    <a className="panel-menu-link" href={info.href} target={info.target}>
+                      {info.title}
+                    </a>
+                  </li>
+                );
+              })}
+            </ul>
+          )}
+      </div>
+    );
+
+    return html;
+  };
+
+  render() {
+    const infoMode: InfoModes | undefined = this.getInfoMode();
+
+    if (!infoMode) {
+      return null;
+    }
+
+    return (
+      <>
+        {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
+          <Tooltip
+            content={this.getInfoContent}
+            className="popper__manager--block"
+            refClassName={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
+          >
+            <i className="fa" />
+            <span className="panel-info-corner-inner" />
+          </Tooltip>
+        ) : null}
+      </>
+    );
+  }
+}
+
+export default PanelHeaderCorner;

+ 2 - 0
public/app/features/dashboard/panel_model.ts

@@ -49,6 +49,8 @@ export class PanelModel {
 
   maxDataPoints?: number;
   interval?: string;
+  description?: string;
+  links?: [];
 
   // non persisted
   fullscreen: boolean;

+ 12 - 0
public/app/features/explore/Explore.tsx

@@ -94,6 +94,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
    * Not kept in component state to prevent edit-render roundtrips.
    */
   queryExpressions: string[];
+  /**
+   * Local ID cache to compare requested vs selected datasource
+   */
+  requestedDatasourceId: string;
 
   constructor(props) {
     super(props);
@@ -167,6 +171,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
     const datasourceId = datasource.meta.id;
     let datasourceError = null;
 
+    // Keep ID to track selection
+    this.requestedDatasourceId = datasourceId;
+
     try {
       const testResult = await datasource.testDatasource();
       datasourceError = testResult.status === 'success' ? null : testResult.message;
@@ -174,6 +181,11 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       datasourceError = (error && error.statusText) || 'Network error';
     }
 
+    if (datasourceId !== this.requestedDatasourceId) {
+      // User already changed datasource again, discard results
+      return;
+    }
+
     const historyKey = `grafana.explore.history.${datasourceId}`;
     const history = store.getObject(historyKey, []);
 

+ 1 - 4
public/app/features/org/OrgDetailsPage.test.tsx

@@ -1,16 +1,13 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 import { OrgDetailsPage, Props } from './OrgDetailsPage';
-import { NavModel, Organization, OrganizationPreferences } from '../../types';
+import { NavModel, Organization } from '../../types';
 
 const setup = (propOverrides?: object) => {
   const props: Props = {
-    preferences: {} as OrganizationPreferences,
     organization: {} as Organization,
     navModel: {} as NavModel,
     loadOrganization: jest.fn(),
-    loadOrganizationPreferences: jest.fn(),
-    loadStarredDashboards: jest.fn(),
     setOrganizationName: jest.fn(),
     updateOrganization: jest.fn(),
   };

+ 8 - 22
public/app/features/org/OrgDetailsPage.tsx

@@ -4,33 +4,22 @@ import { connect } from 'react-redux';
 import PageHeader from '../../core/components/PageHeader/PageHeader';
 import PageLoader from '../../core/components/PageLoader/PageLoader';
 import OrgProfile from './OrgProfile';
-import OrgPreferences from './OrgPreferences';
-import {
-  loadOrganization,
-  loadOrganizationPreferences,
-  setOrganizationName,
-  updateOrganization,
-} from './state/actions';
-import { loadStarredDashboards } from '../../core/actions/user';
-import { NavModel, Organization, OrganizationPreferences, StoreState } from 'app/types';
+import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
+import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions';
+import { NavModel, Organization, StoreState } from 'app/types';
 import { getNavModel } from '../../core/selectors/navModel';
 
 export interface Props {
   navModel: NavModel;
   organization: Organization;
-  preferences: OrganizationPreferences;
   loadOrganization: typeof loadOrganization;
-  loadOrganizationPreferences: typeof loadOrganizationPreferences;
-  loadStarredDashboards: typeof loadStarredDashboards;
   setOrganizationName: typeof setOrganizationName;
   updateOrganization: typeof updateOrganization;
 }
 
 export class OrgDetailsPage extends PureComponent<Props> {
   async componentDidMount() {
-    await this.props.loadStarredDashboards();
     await this.props.loadOrganization();
-    await this.props.loadOrganizationPreferences();
   }
 
   onOrgNameChange = name => {
@@ -42,22 +31,22 @@ export class OrgDetailsPage extends PureComponent<Props> {
   };
 
   render() {
-    const { navModel, organization, preferences } = this.props;
+    const { navModel, organization } = this.props;
+    const isLoading = Object.keys(organization).length === 0;
 
     return (
       <div>
         <PageHeader model={navModel} />
         <div className="page-container page-body">
-          {Object.keys(organization).length === 0 || Object.keys(preferences).length === 0 ? (
-            <PageLoader pageName="Organization" />
-          ) : (
+          {isLoading && <PageLoader pageName="Organization" />}
+          {!isLoading && (
             <div>
               <OrgProfile
                 onOrgNameChange={name => this.onOrgNameChange(name)}
                 onSubmit={this.onUpdateOrganization}
                 orgName={organization.name}
               />
-              <OrgPreferences />
+              <SharedPreferences resourceUri="org" />
             </div>
           )}
         </div>
@@ -70,14 +59,11 @@ function mapStateToProps(state: StoreState) {
   return {
     navModel: getNavModel(state.navIndex, 'org-settings'),
     organization: state.organization.organization,
-    preferences: state.organization.preferences,
   };
 }
 
 const mapDispatchToProps = {
   loadOrganization,
-  loadOrganizationPreferences,
-  loadStarredDashboards,
   setOrganizationName,
   updateOrganization,
 };

+ 0 - 28
public/app/features/org/OrgPreferences.test.tsx

@@ -1,28 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { OrgPreferences, Props } from './OrgPreferences';
-
-const setup = () => {
-  const props: Props = {
-    preferences: {
-      homeDashboardId: 1,
-      timezone: 'UTC',
-      theme: 'Default',
-    },
-    starredDashboards: [{ id: 1, title: 'Standard dashboard', url: '', uri: '', uid: '', type: '', tags: [] }],
-    setOrganizationTimezone: jest.fn(),
-    setOrganizationTheme: jest.fn(),
-    setOrganizationHomeDashboard: jest.fn(),
-    updateOrganizationPreferences: jest.fn(),
-  };
-
-  return shallow(<OrgPreferences {...props} />);
-};
-
-describe('Render', () => {
-  it('should render component', () => {
-    const wrapper = setup();
-
-    expect(wrapper).toMatchSnapshot();
-  });
-});

+ 0 - 113
public/app/features/org/OrgPreferences.tsx

@@ -1,113 +0,0 @@
-import React, { PureComponent } from 'react';
-import { connect } from 'react-redux';
-import { Label } from '../../core/components/Label/Label';
-import SimplePicker from '../../core/components/Picker/SimplePicker';
-import { DashboardSearchHit, OrganizationPreferences } from 'app/types';
-import {
-  setOrganizationHomeDashboard,
-  setOrganizationTheme,
-  setOrganizationTimezone,
-  updateOrganizationPreferences,
-} from './state/actions';
-
-export interface Props {
-  preferences: OrganizationPreferences;
-  starredDashboards: DashboardSearchHit[];
-  setOrganizationHomeDashboard: typeof setOrganizationHomeDashboard;
-  setOrganizationTheme: typeof setOrganizationTheme;
-  setOrganizationTimezone: typeof setOrganizationTimezone;
-  updateOrganizationPreferences: typeof updateOrganizationPreferences;
-}
-
-const themes = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
-
-const timezones = [
-  { value: '', text: 'Default' },
-  { value: 'browser', text: 'Local browser time' },
-  { value: 'utc', text: 'UTC' },
-];
-
-export class OrgPreferences extends PureComponent<Props> {
-  onSubmitForm = event => {
-    event.preventDefault();
-    this.props.updateOrganizationPreferences();
-  };
-
-  render() {
-    const {
-      preferences,
-      starredDashboards,
-      setOrganizationHomeDashboard,
-      setOrganizationTimezone,
-      setOrganizationTheme,
-    } = this.props;
-
-    starredDashboards.unshift({ id: 0, title: 'Default', tags: [], type: '', uid: '', uri: '', url: '' });
-
-    return (
-      <form className="section gf-form-group" onSubmit={this.onSubmitForm}>
-        <h3 className="page-heading">Preferences</h3>
-        <div className="gf-form">
-          <span className="gf-form-label width-11">UI Theme</span>
-          <SimplePicker
-            defaultValue={themes.find(theme => theme.value === preferences.theme)}
-            options={themes}
-            getOptionValue={i => i.value}
-            getOptionLabel={i => i.text}
-            onSelected={theme => setOrganizationTheme(theme.value)}
-            width={20}
-          />
-        </div>
-        <div className="gf-form">
-          <Label
-            width={11}
-            tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
-          >
-            Home Dashboard
-          </Label>
-          <SimplePicker
-            defaultValue={starredDashboards.find(dashboard => dashboard.id === preferences.homeDashboardId)}
-            getOptionValue={i => i.id}
-            getOptionLabel={i => i.title}
-            onSelected={(dashboard: DashboardSearchHit) => setOrganizationHomeDashboard(dashboard.id)}
-            options={starredDashboards}
-            placeholder="Chose default dashboard"
-            width={20}
-          />
-        </div>
-        <div className="gf-form">
-          <label className="gf-form-label width-11">Timezone</label>
-          <SimplePicker
-            defaultValue={timezones.find(timezone => timezone.value === preferences.timezone)}
-            getOptionValue={i => i.value}
-            getOptionLabel={i => i.text}
-            onSelected={timezone => setOrganizationTimezone(timezone.value)}
-            options={timezones}
-            width={20}
-          />
-        </div>
-        <div className="gf-form-button-row">
-          <button type="submit" className="btn btn-success">
-            Save
-          </button>
-        </div>
-      </form>
-    );
-  }
-}
-
-function mapStateToProps(state) {
-  return {
-    preferences: state.organization.preferences,
-    starredDashboards: state.user.starredDashboards,
-  };
-}
-
-const mapDispatchToProps = {
-  setOrganizationHomeDashboard,
-  setOrganizationTimezone,
-  setOrganizationTheme,
-  updateOrganizationPreferences,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(OrgPreferences);

+ 3 - 1
public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap

@@ -29,7 +29,9 @@ exports[`Render should render organization and preferences 1`] = `
         onSubmit={[Function]}
         orgName="Cool org"
       />
-      <Connect(OrgPreferences) />
+      <SharedPreferences
+        resourceUri="org"
+      />
     </div>
   </div>
 </div>

+ 0 - 136
public/app/features/org/__snapshots__/OrgPreferences.test.tsx.snap

@@ -1,136 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Render should render component 1`] = `
-<form
-  className="section gf-form-group"
-  onSubmit={[Function]}
->
-  <h3
-    className="page-heading"
-  >
-    Preferences
-  </h3>
-  <div
-    className="gf-form"
-  >
-    <span
-      className="gf-form-label width-11"
-    >
-      UI Theme
-    </span>
-    <SimplePicker
-      getOptionLabel={[Function]}
-      getOptionValue={[Function]}
-      onSelected={[Function]}
-      options={
-        Array [
-          Object {
-            "text": "Default",
-            "value": "",
-          },
-          Object {
-            "text": "Dark",
-            "value": "dark",
-          },
-          Object {
-            "text": "Light",
-            "value": "light",
-          },
-        ]
-      }
-      width={20}
-    />
-  </div>
-  <div
-    className="gf-form"
-  >
-    <Component
-      tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
-      width={11}
-    >
-      Home Dashboard
-    </Component>
-    <SimplePicker
-      defaultValue={
-        Object {
-          "id": 1,
-          "tags": Array [],
-          "title": "Standard dashboard",
-          "type": "",
-          "uid": "",
-          "uri": "",
-          "url": "",
-        }
-      }
-      getOptionLabel={[Function]}
-      getOptionValue={[Function]}
-      onSelected={[Function]}
-      options={
-        Array [
-          Object {
-            "id": 0,
-            "tags": Array [],
-            "title": "Default",
-            "type": "",
-            "uid": "",
-            "uri": "",
-            "url": "",
-          },
-          Object {
-            "id": 1,
-            "tags": Array [],
-            "title": "Standard dashboard",
-            "type": "",
-            "uid": "",
-            "uri": "",
-            "url": "",
-          },
-        ]
-      }
-      placeholder="Chose default dashboard"
-      width={20}
-    />
-  </div>
-  <div
-    className="gf-form"
-  >
-    <label
-      className="gf-form-label width-11"
-    >
-      Timezone
-    </label>
-    <SimplePicker
-      getOptionLabel={[Function]}
-      getOptionValue={[Function]}
-      onSelected={[Function]}
-      options={
-        Array [
-          Object {
-            "text": "Default",
-            "value": "",
-          },
-          Object {
-            "text": "Local browser time",
-            "value": "browser",
-          },
-          Object {
-            "text": "UTC",
-            "value": "utc",
-          },
-        ]
-      }
-      width={20}
-    />
-  </div>
-  <div
-    className="gf-form-button-row"
-  >
-    <button
-      className="btn btn-success"
-      type="submit"
-    >
-      Save
-    </button>
-  </div>
-</form>
-`;

+ 3 - 70
public/app/features/org/state/actions.ts

@@ -1,16 +1,12 @@
 import { ThunkAction } from 'redux-thunk';
-import { Organization, OrganizationPreferences, StoreState } from 'app/types';
-import { getBackendSrv } from '../../../core/services/backend_srv';
+import { Organization, StoreState } from 'app/types';
+import { getBackendSrv } from 'app/core/services/backend_srv';
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
 
 export enum ActionTypes {
   LoadOrganization = 'LOAD_ORGANISATION',
-  LoadPreferences = 'LOAD_PREFERENCES',
   SetOrganizationName = 'SET_ORGANIZATION_NAME',
-  SetOrganizationTheme = 'SET_ORGANIZATION_THEME',
-  SetOrganizationHomeDashboard = 'SET_ORGANIZATION_HOME_DASHBOARD',
-  SetOrganizationTimezone = 'SET_ORGANIZATION_TIMEZONE',
 }
 
 interface LoadOrganizationAction {
@@ -18,68 +14,22 @@ interface LoadOrganizationAction {
   payload: Organization;
 }
 
-interface LoadPreferencesAction {
-  type: ActionTypes.LoadPreferences;
-  payload: OrganizationPreferences;
-}
-
 interface SetOrganizationNameAction {
   type: ActionTypes.SetOrganizationName;
   payload: string;
 }
 
-interface SetOrganizationThemeAction {
-  type: ActionTypes.SetOrganizationTheme;
-  payload: string;
-}
-
-interface SetOrganizationHomeDashboardAction {
-  type: ActionTypes.SetOrganizationHomeDashboard;
-  payload: number;
-}
-
-interface SetOrganizationTimezoneAction {
-  type: ActionTypes.SetOrganizationTimezone;
-  payload: string;
-}
-
 const organisationLoaded = (organisation: Organization) => ({
   type: ActionTypes.LoadOrganization,
   payload: organisation,
 });
 
-const preferencesLoaded = (preferences: OrganizationPreferences) => ({
-  type: ActionTypes.LoadPreferences,
-  payload: preferences,
-});
-
 export const setOrganizationName = (orgName: string) => ({
   type: ActionTypes.SetOrganizationName,
   payload: orgName,
 });
 
-export const setOrganizationTheme = (theme: string) => ({
-  type: ActionTypes.SetOrganizationTheme,
-  payload: theme,
-});
-
-export const setOrganizationHomeDashboard = (id: number) => ({
-  type: ActionTypes.SetOrganizationHomeDashboard,
-  payload: id,
-});
-
-export const setOrganizationTimezone = (timezone: string) => ({
-  type: ActionTypes.SetOrganizationTimezone,
-  payload: timezone,
-});
-
-export type Action =
-  | LoadOrganizationAction
-  | LoadPreferencesAction
-  | SetOrganizationNameAction
-  | SetOrganizationThemeAction
-  | SetOrganizationHomeDashboardAction
-  | SetOrganizationTimezoneAction;
+export type Action = LoadOrganizationAction | SetOrganizationNameAction;
 
 export function loadOrganization(): ThunkResult<void> {
   return async dispatch => {
@@ -90,13 +40,6 @@ export function loadOrganization(): ThunkResult<void> {
   };
 }
 
-export function loadOrganizationPreferences(): ThunkResult<void> {
-  return async dispatch => {
-    const preferencesResponse = await getBackendSrv().get('/api/org/preferences');
-    dispatch(preferencesLoaded(preferencesResponse));
-  };
-}
-
 export function updateOrganization() {
   return async (dispatch, getStore) => {
     const organization = getStore().organization.organization;
@@ -106,13 +49,3 @@ export function updateOrganization() {
     dispatch(loadOrganization());
   };
 }
-
-export function updateOrganizationPreferences() {
-  return async (dispatch, getStore) => {
-    const preferences = getStore().organization.preferences;
-
-    await getBackendSrv().put('/api/org/preferences', preferences);
-
-    window.location.reload();
-  };
-}

+ 1 - 14
public/app/features/org/state/reducers.ts

@@ -1,9 +1,8 @@
-import { Organization, OrganizationPreferences, OrganizationState } from 'app/types';
+import { Organization, OrganizationState } from 'app/types';
 import { Action, ActionTypes } from './actions';
 
 const initialState: OrganizationState = {
   organization: {} as Organization,
-  preferences: {} as OrganizationPreferences,
 };
 
 const organizationReducer = (state = initialState, action: Action): OrganizationState => {
@@ -11,20 +10,8 @@ const organizationReducer = (state = initialState, action: Action): Organization
     case ActionTypes.LoadOrganization:
       return { ...state, organization: action.payload };
 
-    case ActionTypes.LoadPreferences:
-      return { ...state, preferences: action.payload };
-
     case ActionTypes.SetOrganizationName:
       return { ...state, organization: { ...state.organization, name: action.payload } };
-
-    case ActionTypes.SetOrganizationTheme:
-      return { ...state, preferences: { ...state.preferences, theme: action.payload } };
-
-    case ActionTypes.SetOrganizationHomeDashboard:
-      return { ...state, preferences: { ...state.preferences, homeDashboardId: action.payload } };
-
-    case ActionTypes.SetOrganizationTimezone:
-      return { ...state, preferences: { ...state.preferences, timezone: action.payload } };
   }
 
   return state;

+ 3 - 91
public/app/features/profile/PrefControlCtrl.ts

@@ -1,92 +1,4 @@
-import config from 'app/core/config';
-import coreModule from 'app/core/core_module';
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
 
-export class PrefsControlCtrl {
-  prefs: any;
-  oldTheme: any;
-  prefsForm: any;
-  mode: string;
-
-  timezones: any = [
-    { value: '', text: 'Default' },
-    { value: 'browser', text: 'Local browser time' },
-    { value: 'utc', text: 'UTC' },
-  ];
-  themes: any = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
-
-  /** @ngInject */
-  constructor(private backendSrv, private $location) {}
-
-  $onInit() {
-    return this.backendSrv.get(`/api/${this.mode}/preferences`).then(prefs => {
-      this.prefs = prefs;
-      this.oldTheme = prefs.theme;
-    });
-  }
-
-  updatePrefs() {
-    if (!this.prefsForm.$valid) {
-      return;
-    }
-
-    const cmd = {
-      theme: this.prefs.theme,
-      timezone: this.prefs.timezone,
-      homeDashboardId: this.prefs.homeDashboardId,
-    };
-
-    this.backendSrv.put(`/api/${this.mode}/preferences`, cmd).then(() => {
-      window.location.href = config.appSubUrl + this.$location.path();
-    });
-  }
-}
-
-const template = `
-<form name="ctrl.prefsForm" class="section gf-form-group">
-  <h3 class="page-heading">Preferences</h3>
-
-  <div class="gf-form">
-    <span class="gf-form-label width-11">UI Theme</span>
-    <div class="gf-form-select-wrapper max-width-20">
-      <select class="gf-form-input" ng-model="ctrl.prefs.theme" ng-options="f.value as f.text for f in ctrl.themes"></select>
-    </div>
-  </div>
-
-  <div class="gf-form">
-    <span class="gf-form-label width-11">
-      Home Dashboard
-      <info-popover mode="right-normal">
-        Not finding dashboard you want? Star it first, then it should appear in this select box.
-      </info-popover>
-    </span>
-    <dashboard-selector class="gf-form-select-wrapper max-width-20" model="ctrl.prefs.homeDashboardId">
-    </dashboard-selector>
-  </div>
-
-  <div class="gf-form">
-    <label class="gf-form-label width-11">Timezone</label>
-    <div class="gf-form-select-wrapper max-width-20">
-      <select class="gf-form-input" ng-model="ctrl.prefs.timezone" ng-options="f.value as f.text for f in ctrl.timezones"></select>
-    </div>
-  </div>
-
-  <div class="gf-form-button-row">
-    <button type="submit" class="btn btn-success" ng-click="ctrl.updatePrefs()">Save</button>
-  </div>
-</form>
-`;
-
-export function prefsControlDirective() {
-  return {
-    restrict: 'E',
-    controller: PrefsControlCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    template: template,
-    scope: {
-      mode: '@',
-    },
-  };
-}
-
-coreModule.directive('prefsControl', prefsControlDirective);
+react2AngularDirective('prefsControl', SharedPreferences, ['resourceUri']);

+ 1 - 1
public/app/features/profile/partials/profile.html

@@ -24,7 +24,7 @@
     </div>
   </form>
 
-  <prefs-control mode="user"></prefs-control>
+  <prefs-control resource-uri="'user'"></prefs-control>
 
   <h3 class="page-heading" ng-show="ctrl.showTeamsList">Teams</h3>
   <div class="gf-form-group" ng-show="ctrl.showTeamsList">

+ 6 - 1
public/app/features/teams/TeamPages.test.tsx

@@ -43,10 +43,15 @@ describe('Render', () => {
     expect(wrapper).toMatchSnapshot();
   });
 
-  it('should render settings page', () => {
+  it('should render settings and preferences page', () => {
     const { wrapper } = setup({
       team: getMockTeam(),
       pageName: 'settings',
+      preferences: {
+        homeDashboardId: 1,
+        theme: 'Default',
+        timezone: 'Default',
+      },
     });
 
     expect(wrapper).toMatchSnapshot();

+ 3 - 4
public/app/features/teams/TeamPages.tsx

@@ -41,14 +41,14 @@ export class TeamPages extends PureComponent<Props, State> {
     };
   }
 
-  componentDidMount() {
-    this.fetchTeam();
+  async componentDidMount() {
+    await this.fetchTeam();
   }
 
   async fetchTeam() {
     const { loadTeam, teamId } = this.props;
 
-    await loadTeam(teamId);
+    return await loadTeam(teamId);
   }
 
   getCurrentPage() {
@@ -67,7 +67,6 @@ export class TeamPages extends PureComponent<Props, State> {
 
       case PageTypes.Settings:
         return <TeamSettings />;
-
       case PageTypes.GroupSync:
         return isSyncEnabled && <TeamGroupSync />;
     }

+ 6 - 2
public/app/features/teams/TeamSettings.tsx

@@ -1,10 +1,12 @@
 import React from 'react';
 import { connect } from 'react-redux';
+
 import { Label } from 'app/core/components/Label/Label';
-import { Team } from '../../types';
+import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
 import { updateTeam } from './state/actions';
-import { getRouteParamsId } from '../../core/selectors/location';
+import { getRouteParamsId } from 'app/core/selectors/location';
 import { getTeam } from './state/selectors';
+import { Team } from 'app/types';
 
 export interface Props {
   team: Team;
@@ -41,6 +43,7 @@ export class TeamSettings extends React.Component<Props, State> {
   };
 
   render() {
+    const { team } = this.props;
     const { name, email } = this.state;
 
     return (
@@ -76,6 +79,7 @@ export class TeamSettings extends React.Component<Props, State> {
             </button>
           </div>
         </form>
+        <SharedPreferences resourceUri={`teams/${team.id}`} />
       </div>
     );
   }

+ 1 - 1
public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap

@@ -36,7 +36,7 @@ exports[`Render should render member page if team not empty 1`] = `
 </div>
 `;
 
-exports[`Render should render settings page 1`] = `
+exports[`Render should render settings and preferences page 1`] = `
 <div>
   <PageHeader
     model={Object {}}

+ 3 - 0
public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap

@@ -53,5 +53,8 @@ exports[`Render should render component 1`] = `
       </button>
     </div>
   </form>
+  <SharedPreferences
+    resourceUri="teams/1"
+  />
 </div>
 `;

+ 6 - 5
public/app/features/teams/state/selectors.test.ts

@@ -10,7 +10,6 @@ describe('Teams selectors', () => {
       const mockState: TeamsState = { teams: mockTeams, searchQuery: '', hasFetched: false };
 
       const teams = getTeams(mockState);
-
       expect(teams).toEqual(mockTeams);
     });
 
@@ -18,7 +17,6 @@ describe('Teams selectors', () => {
       const mockState: TeamsState = { teams: mockTeams, searchQuery: '5', hasFetched: false };
 
       const teams = getTeams(mockState);
-
       expect(teams.length).toEqual(1);
     });
   });
@@ -29,10 +27,14 @@ describe('Team selectors', () => {
     const mockTeam = getMockTeam();
 
     it('should return team if matching with location team', () => {
-      const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] };
+      const mockState: TeamState = {
+        team: mockTeam,
+        searchMemberQuery: '',
+        members: [],
+        groups: [],
+      };
 
       const team = getTeam(mockState, '1');
-
       expect(team).toEqual(mockTeam);
     });
   });
@@ -49,7 +51,6 @@ describe('Team selectors', () => {
       };
 
       const members = getTeamMembers(mockState);
-
       expect(members).toEqual(mockTeamMembers);
     });
   });

+ 8 - 4
public/app/plugins/datasource/elasticsearch/config_ctrl.ts

@@ -28,9 +28,13 @@ export class ElasticConfigCtrl {
   ];
 
   indexPatternTypeChanged() {
-    const def = _.find(this.indexPatternTypes, {
-      value: this.current.jsonData.interval,
-    });
-    this.current.database = def.example || 'es-index-name';
+    if (!this.current.database ||
+        this.current.database.length === 0 ||
+        this.current.database.startsWith('[logstash-]')) {
+        const def = _.find(this.indexPatternTypes, {
+          value: this.current.jsonData.interval,
+        });
+        this.current.database = def.example || 'es-index-name';
+    }
   }
 }

+ 6 - 0
public/app/plugins/datasource/elasticsearch/metric_agg.ts

@@ -160,6 +160,12 @@ export class ElasticMetricAggCtrl {
       $scope.agg.settings = {};
       $scope.agg.meta = {};
       $scope.showOptions = false;
+
+      // reset back to metric/group by query
+      if ($scope.target.bucketAggs.length === 0 && $scope.agg.type !== 'raw_document') {
+        $scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
+      }
+
       $scope.updatePipelineAggOptions();
       $scope.onChange();
     };

+ 2 - 2
public/app/plugins/datasource/elasticsearch/query_builder.ts

@@ -181,8 +181,8 @@ export class ElasticQueryBuilder {
 
   build(target, adhocFilters?, queryString?) {
     // make sure query has defaults;
-    target.metrics = target.metrics || [{ type: 'count', id: '1' }];
-    target.bucketAggs = target.bucketAggs || [{ type: 'date_histogram', id: '2', settings: { interval: 'auto' } }];
+    target.metrics = target.metrics || [queryDef.defaultMetricAgg()];
+    target.bucketAggs = target.bucketAggs || [queryDef.defaultBucketAgg()];
     target.timeField = this.timeField;
 
     let i, nestedAggs, metric;

+ 13 - 0
public/app/plugins/datasource/elasticsearch/query_ctrl.ts

@@ -17,6 +17,19 @@ export class ElasticQueryCtrl extends QueryCtrl {
     super($scope, $injector);
 
     this.esVersion = this.datasource.esVersion;
+
+    this.target = this.target || {};
+    this.target.metrics = this.target.metrics || [queryDef.defaultMetricAgg()];
+    this.target.bucketAggs = this.target.bucketAggs || [queryDef.defaultBucketAgg()];
+
+    if (this.target.bucketAggs.length === 0) {
+      const metric = this.target.metrics[0];
+      if (!metric || metric.type !== 'raw_document') {
+        this.target.bucketAggs = [queryDef.defaultBucketAgg()];
+      }
+      this.refresh();
+    }
+
     this.queryUpdated();
   }
 

+ 8 - 0
public/app/plugins/datasource/elasticsearch/query_def.ts

@@ -228,3 +228,11 @@ export function describeOrderBy(orderBy, target) {
     return 'metric not found';
   }
 }
+
+export function defaultMetricAgg() {
+  return { type: 'count', id: '1' };
+}
+
+export function defaultBucketAgg() {
+  return { type: 'date_histogram', id: '2', settings: { interval: 'auto' } };
+}

+ 1 - 2
public/app/types/index.ts

@@ -22,7 +22,7 @@ import {
 } from './series';
 import { PanelProps, PanelOptionsProps } from './panel';
 import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
-import { Organization, OrganizationPreferences, OrganizationState } from './organization';
+import { Organization, OrganizationState } from './organization';
 import {
   AppNotification,
   AppNotificationSeverity,
@@ -83,7 +83,6 @@ export {
   PluginDashboard,
   Organization,
   OrganizationState,
-  OrganizationPreferences,
   AppNotification,
   AppNotificationsState,
   AppNotificationSeverity,

+ 0 - 7
public/app/types/organization.ts

@@ -3,13 +3,6 @@ export interface Organization {
   id: number;
 }
 
-export interface OrganizationPreferences {
-  homeDashboardId: number;
-  theme: string;
-  timezone: string;
-}
-
 export interface OrganizationState {
   organization: Organization;
-  preferences: OrganizationPreferences;
 }

+ 18 - 10
public/sass/components/_popper.scss

@@ -1,12 +1,8 @@
 .popper {
   position: absolute;
   z-index: $zindex-tooltip;
-  background: $tooltipBackground;
   color: $tooltipColor;
   max-width: 400px;
-  border-radius: 3px;
-  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
-  padding: 10px;
   text-align: center;
 }
 
@@ -35,10 +31,18 @@
   left: calc(50% - 5px);
   margin-top: 0;
   margin-bottom: 0;
+  padding-top: 5px;
 }
 
 .popper[data-placement^='bottom'] {
-  margin-top: 5px;
+  padding-top: 5px;
+}
+
+.popper__background {
+  background: $tooltipBackground;
+  border-radius: 3px;
+  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
+  padding: 10px;
 }
 
 .popper[data-placement^='bottom'] .popper__arrow {
@@ -46,21 +50,21 @@
   border-left-color: transparent;
   border-right-color: transparent;
   border-top-color: transparent;
-  top: -5px;
-  left: calc(50% - 5px);
+  top: 0;
+  left: calc(50% - 8px);
   margin-top: 0;
   margin-bottom: 0;
 }
 .popper[data-placement^='right'] {
-  margin-left: 5px;
+  padding-left: 5px;
 }
 .popper[data-placement^='right'] .popper__arrow {
   border-width: 5px 5px 5px 0;
   border-left-color: transparent;
   border-top-color: transparent;
   border-bottom-color: transparent;
-  left: -5px;
-  top: calc(50% - 5px);
+  left: 0;
+  top: calc(50% - 8px);
   margin-left: 0;
   margin-right: 0;
 }
@@ -84,3 +88,7 @@
 .popper__manager {
   display: inline-block;
 }
+
+.popper__manager--block {
+  display: block;
+}

+ 13 - 1
public/sass/pages/_explore.scss

@@ -320,8 +320,11 @@
 
 .ReactTable {
   border: none;
+}
+
+.ReactTable .rt-table {
   // Allow some space for the no-data text
-  min-height: 120px;
+  min-height: 90px;
 }
 
 .ReactTable .rt-thead.-header {
@@ -350,6 +353,11 @@
 .ReactTable .rt-tbody .rt-td:last-child {
   border-right: none;
 }
+.ReactTable .-pagination {
+  border-top: none;
+  box-shadow: none;
+  margin-top: $panel-margin;
+}
 .ReactTable .-pagination .-btn {
   color: $blue;
   background: $list-item-bg;
@@ -371,6 +379,10 @@
 .ReactTable .rt-tr .rt-td:last-child {
   text-align: right;
 }
+.ReactTable .rt-noData {
+  top: 60px;
+  z-index: inherit;
+}
 
 // React-component cascade fix: show "loading" even though item can expand
 

+ 4 - 0
public/sass/utils/_utils.scss

@@ -78,3 +78,7 @@ button.close {
 .d-inline-block {
   display: inline-block;
 }
+
+.absolute {
+  position: absolute;
+}

+ 130 - 25
yarn.lock

@@ -365,7 +365,7 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react-dom@*", "@types/react-dom@^16.0.7":
+"@types/react-dom@*":
   version "16.0.8"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.8.tgz#6e1366ed629cadf55860cbfcc25db533f5d2fa7d"
   integrity sha512-WF/KAOia7pskV+J8f+UlNuFeCRkJuJAkyyeYPPtNe6suw0y7cWyUP/DPdPXsGUwQEkv2qlLVSrgVaoCm/PmO0Q==
@@ -373,6 +373,14 @@
     "@types/node" "*"
     "@types/react" "*"
 
+"@types/react-dom@^16.0.9":
+  version "16.0.9"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.9.tgz#73ceb7abe6703822eab6600e65c5c52efd07fb91"
+  integrity sha512-4Z0bW+75zeQgsEg7RaNuS1k9MKhci7oQqZXxrV5KUGIyXZHHAAL3KA4rjhdH8o6foZ5xsRMSqkoM5A3yRVPR5w==
+  dependencies:
+    "@types/node" "*"
+    "@types/react" "*"
+
 "@types/react-select@^2.0.4":
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.4.tgz#232c735539412acdc163751157c0a1c7d8aca40b"
@@ -389,7 +397,7 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react@*", "@types/react@^16.4.14":
+"@types/react@*":
   version "16.4.16"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.16.tgz#99f91b1200ae8c2062030402006d3b3c3a177043"
   integrity sha512-lxyoipLWweAnLnSsV4Ho2NAZTKKmxeYgkTQ6PaDiPDU9JJBUY2zJVVGiK1smzYv8+ZgbqEmcm5xM74GCpunSEA==
@@ -397,6 +405,14 @@
     "@types/prop-types" "*"
     csstype "^2.2.0"
 
+"@types/react@^16.1.0", "@types/react@^16.7.6":
+  version "16.7.6"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
+  integrity sha512-QBUfzftr/8eg/q3ZRgf/GaDP6rTYc7ZNem+g4oZM38C9vXyV8AWRWaTQuW5yCoZTsfHrN7b3DeEiUnqH9SrnpA==
+  dependencies:
+    "@types/prop-types" "*"
+    csstype "^2.2.0"
+
 "@types/tapable@^0":
   version "0.2.5"
   resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-0.2.5.tgz#2443fc12da514c81346b1a665675559cee21fa75"
@@ -1031,7 +1047,7 @@ arrify@^1.0.0, arrify@^1.0.1:
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
 
-asap@^2.0.0:
+asap@^2.0.0, asap@~2.0.3:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
   integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -1854,7 +1870,7 @@ babel-register@^6.26.0, babel-register@^6.9.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.15"
 
-babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+babel-runtime@6.x, babel-runtime@6.x.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
@@ -3148,6 +3164,11 @@ copy-descriptor@^0.1.0:
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
+core-js@^1.0.0:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+  integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
 core-js@^2.0.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
   version "2.5.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
@@ -3243,6 +3264,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
+create-react-context@^0.2.1:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
+  integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==
+  dependencies:
+    fbjs "^0.8.0"
+    gud "^1.0.0"
+
 cross-spawn@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
@@ -5090,6 +5119,19 @@ fb-watchman@^2.0.0:
   dependencies:
     bser "^2.0.0"
 
+fbjs@^0.8.0:
+  version "0.8.17"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+  integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+  dependencies:
+    core-js "^1.0.0"
+    isomorphic-fetch "^2.1.1"
+    loose-envify "^1.0.0"
+    object-assign "^4.1.0"
+    promise "^7.1.1"
+    setimmediate "^1.0.5"
+    ua-parser-js "^0.7.18"
+
 fd-slicer@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
@@ -6067,6 +6109,11 @@ grunt@1.0.1:
     path-is-absolute "~1.0.0"
     rimraf "~2.2.8"
 
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+
 gzip-size@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-1.0.0.tgz#66cf8b101047227b95bace6ea1da0c177ed5c22f"
@@ -7247,6 +7294,14 @@ isomorphic-base64@^1.0.2:
   resolved "https://registry.yarnpkg.com/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803"
   integrity sha1-9Caq6CVpuopOxcpzrSGkSrHueAM=
 
+isomorphic-fetch@^2.1.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+  integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+  dependencies:
+    node-fetch "^1.0.1"
+    whatwg-fetch ">=0.10.0"
+
 isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -9211,6 +9266,14 @@ node-fetch-npm@^2.0.2:
     json-parse-better-errors "^1.0.0"
     safe-buffer "^5.1.1"
 
+node-fetch@^1.0.1:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+  integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+  dependencies:
+    encoding "^0.1.11"
+    is-stream "^1.0.1"
+
 node-forge@0.7.5:
   version "0.7.5"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
@@ -10383,10 +10446,10 @@ pn@^1.1.0:
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
   integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
 
-popper.js@^1.12.5:
-  version "1.14.4"
-  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6"
-  integrity sha1-juwdj/AqWjoVLdQ0FKFce3n9abY=
+popper.js@^1.14.4:
+  version "1.14.5"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.5.tgz#98abcce7c7c34c4ee47fcbc6b3da8af2c0a127bc"
+  integrity sha512-fs4Sd8bZLgEzrk8aS7Em1qh+wcawtE87kRUJQhK6+LndyV1HerX7+LURzAylVaTyWIn5NTB/lyjnWqw/AZ6Yrw==
 
 portfinder@^1.0.9:
   version "1.0.17"
@@ -10953,6 +11016,13 @@ promise-retry@^1.1.1:
     err-code "^1.0.0"
     retry "^0.10.0"
 
+promise@^7.1.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+  dependencies:
+    asap "~2.0.3"
+
 prompts@^0.1.9:
   version "0.1.14"
   resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2"
@@ -11270,15 +11340,15 @@ react-custom-scrollbars@^4.2.1:
     prop-types "^15.5.10"
     raf "^3.1.0"
 
-react-dom@^16.5.0:
-  version "16.5.2"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
-  integrity sha512-RC8LDw8feuZOHVgzEf7f+cxBr/DnKdqp56VU0lAs1f4UfKc4cU8wU4fTq/mgnvynLQo8OtlPC19NUFh/zjZPuA==
+react-dom@^16.6.3:
+  version "16.6.3"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0"
+  integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
     prop-types "^15.6.2"
-    schedule "^0.5.0"
+    scheduler "^0.11.2"
 
 react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3":
   version "3.0.5"
@@ -11341,13 +11411,18 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
-react-popper@^0.7.5:
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596"
-  integrity sha512-ya9dhhGCf74JTOB2uyksEHhIGw7w9tNZRUJF73lEq2h4H5JT6MBa4PdT4G+sx6fZwq+xKZAL/sVNAIuojPn7Dg==
+react-popper@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.0.tgz#e769199bbe1273611957892f9950ef1d42c3f7ce"
+  integrity sha512-Dbn9kwgFzNFRi8yz/i4Qp7d1hkCYhWX6uJOFz0+PoNNm9uJMnFAqSPNgUUCV49L6p5zz5mKtMiudbgIqjAe1uw==
   dependencies:
-    popper.js "^1.12.5"
-    prop-types "^15.5.10"
+    "@types/react" "^16.1.0"
+    babel-runtime "6.x.x"
+    create-react-context "^0.2.1"
+    popper.js "^1.14.4"
+    prop-types "^15.6.1"
+    typed-styles "^0.0.5"
+    warning "^3.0.0"
 
 react-portal@^3.1.0:
   version "3.2.0"
@@ -11439,15 +11514,15 @@ react-virtualized@^9.21.0:
     prop-types "^15.6.0"
     react-lifecycles-compat "^3.0.4"
 
-react@^16.5.0:
-  version "16.5.2"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
-  integrity sha512-FDCSVd3DjVTmbEAjUNX6FgfAmQ+ypJfHUsqUJOYNCBUp1h8lqmtC+0mXJ+JjsWx4KAVTkk1vKd1hLQPvEviSuw==
+react@^16.6.3:
+  version "16.6.3"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
+  integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
     prop-types "^15.6.2"
-    schedule "^0.5.0"
+    scheduler "^0.11.2"
 
 read-chunk@^2.1.0:
   version "2.1.0"
@@ -12245,6 +12320,14 @@ schedule@^0.5.0:
   dependencies:
     object-assign "^4.1.1"
 
+scheduler@^0.11.2:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.2.tgz#a8db5399d06eba5abac51b705b7151d2319d33d3"
+  integrity sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
 schema-utils@^0.4.0, schema-utils@^0.4.4, schema-utils@^0.4.5:
   version "0.4.7"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
@@ -12399,7 +12482,7 @@ set-value@^2.0.0:
     is-plain-object "^2.0.3"
     split-string "^3.0.1"
 
-setimmediate@^1.0.4:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
   integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -13701,6 +13784,11 @@ type-of@^2.0.1:
   resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972"
   integrity sha1-5yoXQYllaOn2KDeNgW1pEvfyOXI=
 
+typed-styles@^0.0.5:
+  version "0.0.5"
+  resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.5.tgz#a60df245d482a9b1adf9c06c078d0f06085ed1cf"
+  integrity sha512-ht+rEe5UsdEBAa3gr64+QjUOqjOLJfWLvl5HZR5Ev9uo/OnD3p43wPeFSB1hNFc13GXQF/JU1Bn0YHLUqBRIlw==
+
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -13711,6 +13799,11 @@ typescript@^3.0.3:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.1.tgz#3362ba9dd1e482ebb2355b02dfe8bcd19a2c7c96"
   integrity sha512-Veu0w4dTc/9wlWNf2jeRInNodKlcdLgemvPsrNpfu5Pq39sgfFjvIIgTsvUHCoLBnMhPoUA+tFxsXjU6VexVRQ==
 
+ua-parser-js@^0.7.18:
+  version "0.7.19"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
+  integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
+
 uglify-es@^3.3.4:
   version "3.3.9"
   resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
@@ -14125,6 +14218,13 @@ walker@~1.0.5:
   dependencies:
     makeerror "1.0.x"
 
+warning@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
+  integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=
+  dependencies:
+    loose-envify "^1.0.0"
+
 warning@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607"
@@ -14359,6 +14459,11 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
   dependencies:
     iconv-lite "0.4.24"
 
+whatwg-fetch@>=0.10.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+
 whatwg-mimetype@^2.1.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171"