Compare commits
53 Commits
v3.0-beta1
...
v3.0-beta2
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ebc172cb | |||
| 892e79a982 | |||
| 542e080f0a | |||
| f35eb0f148 | |||
| c764e7fd97 | |||
| 1c97f10d8a | |||
| d8499e6941 | |||
| 105a678d64 | |||
| a12de09533 | |||
| 431a610f00 | |||
| 6e6ebc5947 | |||
| b30b78e442 | |||
| cab859a0e4 | |||
| a2c6469d41 | |||
| 55d95f9009 | |||
| ab1048b7ee | |||
| 38a10f8be4 | |||
| f165ba6480 | |||
| bcc875a39c | |||
| 2f2029a5c1 | |||
| 8dd509c31b | |||
| 171f6422b1 | |||
| 5e6c3f314c | |||
| efc97d177a | |||
| 601e90f5fe | |||
| decede394a | |||
| eb850ddd36 | |||
| 4a807b6373 | |||
| 6ed9164c9d | |||
| ef89dabb61 | |||
| 9be1ffd633 | |||
| 5e431149cd | |||
| 7023dedc05 | |||
| 9e98c86ae1 | |||
| 071237d322 | |||
| cb42cfc6af | |||
| 4fbe954a79 | |||
| 66621d762e | |||
| 9dd6aefcec | |||
| 749fd618a9 | |||
| a88176e060 | |||
| e06756bcab | |||
| 9c222a445f | |||
| 51bde36dee | |||
| e371e03696 | |||
| 0bf721a74c | |||
| 43b474143c | |||
| 02221c993e | |||
| 9c8d508247 | |||
| 8f42bec270 | |||
| 660d3fa1e9 | |||
| 1ef332e82c | |||
| cf926134ef |
+16
-1
@@ -1,4 +1,19 @@
|
||||
# 3.0.0 (unrelased master branch)
|
||||
# 3.0.0-beta2 (unreleased)
|
||||
|
||||
### New Features (introduces since 3.0-beta1)
|
||||
* **Preferences**: Set home dashboard on user and org level, closes [#1678](https://github.com/grafana/grafana/issues/1678)
|
||||
* **Preferences**: Set timezone on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1200](https://github.com/grafana/grafana/issues/1200)
|
||||
* **Preferences**: Set theme on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1917](https://github.com/grafana/grafana/issues/1917)
|
||||
|
||||
### Bug fixes
|
||||
* **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529)
|
||||
* **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532)
|
||||
* **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536)
|
||||
* **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4540](https://github.com/grafana/grafana/issues/4540)
|
||||
* **Graph**: Fix legend in table mode with series on right-y axis, fixes [#4551](https://github.com/grafana/grafana/issues/4551), [#1145](https://github.com/grafana/grafana/issues/1145)
|
||||
* **Password**: Password reset link/page did not work, fixes [#4542](https://github.com/grafana/grafana/issues/4542)
|
||||
|
||||
# 3.0.0-beta1 (2016-03-31)
|
||||
|
||||
### New Features
|
||||
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ os: Windows Server 2012 R2
|
||||
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
|
||||
environment:
|
||||
nodejs_version: "4"
|
||||
nodejs_version: "5"
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.0.0
|
||||
3.0.0
|
||||
|
||||
@@ -10,14 +10,21 @@ page_keywords: grafana, installation, debian, ubuntu, guide
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
.deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
|
||||
Stable .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
|
||||
Beta .deb for Debian-based Linux | [grafana_3.0.0-beta11459429091_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta11459429091_amd64.deb)
|
||||
|
||||
## Install
|
||||
## Install Stable
|
||||
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb
|
||||
$ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_2.6.0_amd64.deb
|
||||
|
||||
## Install 3.0 Beta
|
||||
|
||||
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta11459429091_amd64.deb
|
||||
$ sudo apt-get install -y adduser libfontconfig
|
||||
$ sudo dpkg -i grafana_3.0.0-beta11459429091_amd64.deb
|
||||
|
||||
## APT Repository
|
||||
|
||||
Add the following line to your `/etc/apt/sources.list` file.
|
||||
|
||||
@@ -10,9 +10,10 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm)
|
||||
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm)
|
||||
Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta11459429091.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta11459429091.x86_64.rpm)
|
||||
|
||||
## Install from package file
|
||||
## Install Stable Release from package file
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
@@ -29,6 +30,24 @@ Or install manually using `rpm`.
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm
|
||||
|
||||
## Install Beta Release from package file
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta11459429091.x86_64.rpm
|
||||
|
||||
Or install manually using `rpm`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-3.0.0-beta11459429091.x86_64.rpm
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-3.0.0-beta11459429091.x86_64.rpm
|
||||
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
|
||||
|
||||
@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip)
|
||||
Stable Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip)
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ module.exports = function(config) {
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'vendor/npm/es6-shim/es6-shim.js',
|
||||
'vendor/npm/es6-promise/dist/es6-promise.js',
|
||||
'vendor/npm/systemjs/dist/system.src.js',
|
||||
'test/test-main.js',
|
||||
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "3.0.0-beta1",
|
||||
"version": "3.0.0-beta2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@@ -32,7 +32,7 @@
|
||||
"grunt-contrib-watch": "^0.6.1",
|
||||
"grunt-filerev": "^0.2.1",
|
||||
"grunt-git-describe": "~2.3.2",
|
||||
"grunt-karma": "~0.12.1",
|
||||
"grunt-karma": "~0.12.2",
|
||||
"grunt-ng-annotate": "^1.0.1",
|
||||
"grunt-notify": "^0.4.3",
|
||||
"grunt-postcss": "^0.8.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"grunt-typescript": "^0.8.0",
|
||||
"grunt-usemin": "3.0.0",
|
||||
"jshint-stylish": "~2.1.0",
|
||||
"karma": "~0.13.15",
|
||||
"karma": "0.13.22",
|
||||
"karma-chrome-launcher": "~0.2.2",
|
||||
"karma-coverage": "0.5.3",
|
||||
"karma-coveralls": "1.1.2",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
deb_ver=3.0.0-pre1459399258
|
||||
rpm_ver=3.0.0-pre1459399258
|
||||
deb_ver=3.0.0-beta11459429091
|
||||
rpm_ver=3.0.0-beta11459429091
|
||||
|
||||
#rpm_ver=3.0.0-1
|
||||
|
||||
#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
|
||||
|
||||
# package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb
|
||||
# package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb
|
||||
|
||||
@@ -29,6 +29,7 @@ func Register(r *macaron.Macaron) {
|
||||
|
||||
// authed views
|
||||
r.Get("/profile/", reqSignedIn, Index)
|
||||
r.Get("/profile/password", reqSignedIn, Index)
|
||||
r.Get("/org/", reqSignedIn, Index)
|
||||
r.Get("/org/new", reqSignedIn, Index)
|
||||
r.Get("/datasources/", reqSignedIn, Index)
|
||||
@@ -96,10 +97,15 @@ func Register(r *macaron.Macaron) {
|
||||
r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
|
||||
r.Post("/using/:id", wrap(UserSetUsingOrg))
|
||||
r.Get("/orgs", wrap(GetSignedInUserOrgList))
|
||||
|
||||
r.Post("/stars/dashboard/:id", wrap(StarDashboard))
|
||||
r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
|
||||
|
||||
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
|
||||
r.Get("/quotas", wrap(GetUserQuotas))
|
||||
|
||||
r.Get("/preferences", wrap(GetUserPreferences))
|
||||
r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
|
||||
})
|
||||
|
||||
// users (admin permission required)
|
||||
@@ -130,6 +136,9 @@ func Register(r *macaron.Macaron) {
|
||||
r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
|
||||
r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
|
||||
|
||||
// prefs
|
||||
r.Get("/preferences", wrap(GetOrgPreferences))
|
||||
r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// create new org
|
||||
@@ -164,6 +173,11 @@ func Register(r *macaron.Macaron) {
|
||||
r.Delete("/:id", wrap(DeleteApiKey))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// Preferences
|
||||
r.Group("/preferences", func() {
|
||||
r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
|
||||
})
|
||||
|
||||
// Data sources
|
||||
r.Group("/datasources", func() {
|
||||
r.Get("/", GetDataSources)
|
||||
|
||||
@@ -159,6 +159,24 @@ func canEditDashboard(role m.RoleType) bool {
|
||||
}
|
||||
|
||||
func GetHomeDashboard(c *middleware.Context) {
|
||||
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
|
||||
if err := bus.Dispatch(&prefsQuery); err != nil {
|
||||
c.JsonApiErr(500, "Failed to get preferences", err)
|
||||
}
|
||||
|
||||
if prefsQuery.Result.HomeDashboardId != 0 {
|
||||
slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
|
||||
err := bus.Dispatch(&slugQuery)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to get slug from database", err)
|
||||
return
|
||||
}
|
||||
|
||||
dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
|
||||
c.JSON(200, &dashRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -33,6 +33,7 @@ type CurrentUser struct {
|
||||
OrgRole m.RoleType `json:"orgRole"`
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type DashboardMeta struct {
|
||||
@@ -57,6 +58,10 @@ type DashboardFullWithMeta struct {
|
||||
Dashboard *simplejson.Json `json:"dashboard"`
|
||||
}
|
||||
|
||||
type DashboardRedirect struct {
|
||||
RedirectUri string `json:"redirectUri"`
|
||||
}
|
||||
|
||||
type DataSource struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package dtos
|
||||
|
||||
type Prefs struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardId int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type UpdatePrefsCmd struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardId int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
defaultDatasource = ds.Name
|
||||
}
|
||||
|
||||
if len(ds.JsonData.MustMap()) > 0 {
|
||||
if ds.JsonData != nil {
|
||||
dsMap["jsonData"] = ds.JsonData
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -14,6 +15,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
|
||||
if err := bus.Dispatch(&prefsQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefs := prefsQuery.Result
|
||||
|
||||
var data = dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserId,
|
||||
@@ -21,12 +28,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
LightTheme: c.Theme == "light",
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == "light",
|
||||
Timezone: prefs.Timezone,
|
||||
},
|
||||
Settings: settings,
|
||||
AppUrl: setting.AppUrl,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// POST /api/preferences/set-home-dash
|
||||
func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response {
|
||||
|
||||
cmd.UserId = c.UserId
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to set home dashboard", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Home dashboard set")
|
||||
}
|
||||
|
||||
// GET /api/user/preferences
|
||||
func GetUserPreferences(c *middleware.Context) Response {
|
||||
return getPreferencesFor(c.OrgId, c.UserId)
|
||||
}
|
||||
|
||||
func getPreferencesFor(orgId int64, userId int64) Response {
|
||||
prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId}
|
||||
|
||||
if err := bus.Dispatch(&prefsQuery); err != nil {
|
||||
return ApiError(500, "Failed to get preferences", err)
|
||||
}
|
||||
|
||||
dto := dtos.Prefs{
|
||||
Theme: prefsQuery.Result.Theme,
|
||||
HomeDashboardId: prefsQuery.Result.HomeDashboardId,
|
||||
Timezone: prefsQuery.Result.Timezone,
|
||||
}
|
||||
|
||||
return Json(200, &dto)
|
||||
}
|
||||
|
||||
// PUT /api/user/preferences
|
||||
func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response {
|
||||
return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
|
||||
}
|
||||
|
||||
func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
|
||||
saveCmd := m.SavePreferencesCommand{
|
||||
UserId: userId,
|
||||
OrgId: orgId,
|
||||
Theme: dtoCmd.Theme,
|
||||
Timezone: dtoCmd.Timezone,
|
||||
HomeDashboardId: dtoCmd.HomeDashboardId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveCmd); err != nil {
|
||||
return ApiError(500, "Failed to save preferences", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Preferences updated")
|
||||
}
|
||||
|
||||
// GET /api/org/preferences
|
||||
func GetOrgPreferences(c *middleware.Context) Response {
|
||||
return getPreferencesFor(c.OrgId, 0)
|
||||
}
|
||||
|
||||
// PUT /api/org/preferences
|
||||
func UpdateOrgPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response {
|
||||
return updatePreferencesFor(c.OrgId, 0, &dtoCmd)
|
||||
}
|
||||
@@ -148,3 +148,8 @@ type GetDashboardsQuery struct {
|
||||
DashboardIds []int64
|
||||
Result *[]Dashboard
|
||||
}
|
||||
|
||||
type GetDashboardSlugByIdQuery struct {
|
||||
Id int64
|
||||
Result string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrPreferencesNotFound = errors.New("Preferences not found")
|
||||
)
|
||||
|
||||
type Preferences struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
UserId int64
|
||||
Version int
|
||||
HomeDashboardId int64
|
||||
Timezone string
|
||||
Theme string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
|
||||
type GetPreferencesQuery struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
UserId int64
|
||||
|
||||
Result *Preferences
|
||||
}
|
||||
|
||||
type GetPreferencesWithDefaultsQuery struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
UserId int64
|
||||
|
||||
Result *Preferences
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
type SavePreferencesCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
|
||||
HomeDashboardId int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
@@ -136,7 +136,6 @@ type SignedInUser struct {
|
||||
Login string
|
||||
Name string
|
||||
Email string
|
||||
Theme string
|
||||
ApiKeyId int64
|
||||
IsGrafanaAdmin bool
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ func init() {
|
||||
bus.AddHandler("sql", DeleteDashboard)
|
||||
bus.AddHandler("sql", SearchDashboards)
|
||||
bus.AddHandler("sql", GetDashboardTags)
|
||||
bus.AddHandler("sql", GetDashboardSlugById)
|
||||
}
|
||||
|
||||
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
@@ -255,3 +256,23 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DashboardSlugDTO struct {
|
||||
Slug string
|
||||
}
|
||||
|
||||
func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
|
||||
var rawSql = `SELECT slug from dashboard WHERE Id=?`
|
||||
var slug = DashboardSlugDTO{}
|
||||
|
||||
exists, err := x.Sql(rawSql, query.Id).Get(&slug)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists == false {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
query.Result = slug.Slug
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,17 +4,27 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addPreferencesMigrations(mg *Migrator) {
|
||||
|
||||
preferencesV1 := Table{
|
||||
mg.AddMigration("drop preferences table v2", NewDropTableMigration("preferences"))
|
||||
|
||||
preferencesV2 := Table{
|
||||
Name: "preferences",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "pref_id", Type: DB_Int, Nullable: false},
|
||||
{Name: "pref_type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "pref_data", Type: DB_Text, Nullable: false},
|
||||
{Name: "org_id", Type: DB_Int, Nullable: true},
|
||||
{Name: "user_id", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "home_dashboard_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: true},
|
||||
{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: true},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"org_id"}},
|
||||
{Cols: []string{"user_id"}},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create preferences table v1", NewAddTableMigration(preferencesV1))
|
||||
|
||||
mg.AddMigration("create preferences table v2", NewAddTableMigration(preferencesV2))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetPreferences)
|
||||
bus.AddHandler("sql", GetPreferencesWithDefaults)
|
||||
bus.AddHandler("sql", SavePreferences)
|
||||
}
|
||||
|
||||
func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error {
|
||||
|
||||
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").
|
||||
Find(&prefs)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := &m.Preferences{
|
||||
Theme: "dark",
|
||||
Timezone: "browser",
|
||||
HomeDashboardId: 0,
|
||||
}
|
||||
|
||||
for _, p := range prefs {
|
||||
if p.Theme != "" {
|
||||
res.Theme = p.Theme
|
||||
}
|
||||
if p.Timezone != "" {
|
||||
res.Timezone = p.Timezone
|
||||
}
|
||||
if p.HomeDashboardId != 0 {
|
||||
res.HomeDashboardId = p.HomeDashboardId
|
||||
}
|
||||
}
|
||||
|
||||
query.Result = res
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
query.Result = &prefs
|
||||
} else {
|
||||
query.Result = new(m.Preferences)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SavePreferences(cmd *m.SavePreferencesCommand) error {
|
||||
return inTransaction2(func(sess *session) error {
|
||||
|
||||
var prefs m.Preferences
|
||||
exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
|
||||
|
||||
if !exists {
|
||||
prefs = m.Preferences{
|
||||
UserId: cmd.UserId,
|
||||
OrgId: cmd.OrgId,
|
||||
HomeDashboardId: cmd.HomeDashboardId,
|
||||
Timezone: cmd.Timezone,
|
||||
Theme: cmd.Theme,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
_, err = sess.Insert(&prefs)
|
||||
return err
|
||||
} else {
|
||||
prefs.HomeDashboardId = cmd.HomeDashboardId
|
||||
prefs.Timezone = cmd.Timezone
|
||||
prefs.Theme = cmd.Theme
|
||||
prefs.Updated = time.Now()
|
||||
prefs.Version += 1
|
||||
_, err := sess.Id(prefs.Id).AllCols().Update(&prefs)
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -273,7 +273,6 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||
u.email as email,
|
||||
u.login as login,
|
||||
u.name as name,
|
||||
u.theme as theme,
|
||||
org.name as org_name,
|
||||
org_user.role as org_role,
|
||||
org.id as org_id
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
|
||||
`;
|
||||
|
||||
export class DashboardSelectorCtrl {
|
||||
model: any;
|
||||
options: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv) {
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.options = [{value: 0, text: 'Default'}];
|
||||
|
||||
return this.backendSrv.search({starred: true}).then(res => {
|
||||
res.forEach(dash => {
|
||||
this.options.push({value: dash.id, text: dash.title});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function dashboardSelector() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: DashboardSelectorCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
template: template,
|
||||
scope: {
|
||||
model: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashboardSelector', dashboardSelector);
|
||||
@@ -32,6 +32,7 @@ import {liveSrv} from './live/live_srv';
|
||||
import {Emitter} from './utils/emitter';
|
||||
import {layoutSelector} from './components/layout_selector/layout_selector';
|
||||
import {switchDirective} from './components/switch';
|
||||
import {dashboardSelector} from './components/dashboard_selector';
|
||||
import 'app/core/controllers/all';
|
||||
import 'app/core/services/all';
|
||||
import 'app/core/routes/routes';
|
||||
@@ -54,4 +55,5 @@ export {
|
||||
infoPopover,
|
||||
Emitter,
|
||||
appEvents,
|
||||
dashboardSelector,
|
||||
};
|
||||
|
||||
@@ -4,13 +4,17 @@ define([
|
||||
function (coreModule) {
|
||||
"use strict";
|
||||
|
||||
coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv) {
|
||||
coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
|
||||
|
||||
if (!$routeParams.slug) {
|
||||
backendSrv.get('/api/dashboards/home').then(function(result) {
|
||||
var meta = result.meta;
|
||||
meta.canSave = meta.canShare = meta.canStar = false;
|
||||
$scope.initDashboard(result, $scope);
|
||||
backendSrv.get('/api/dashboards/home').then(function(homeDash) {
|
||||
if (homeDash.redirectUri) {
|
||||
$location.path('dashboard/' + homeDash.redirectUri);
|
||||
} else {
|
||||
var meta = homeDash.meta;
|
||||
meta.canSave = meta.canShare = meta.canStar = false;
|
||||
$scope.initDashboard(homeDash, $scope);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,16 +88,20 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/profile', {
|
||||
templateUrl: 'public/app/features/profile/partials/profile.html',
|
||||
templateUrl: 'public/app/features/org/partials/profile.html',
|
||||
controller : 'ProfileCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/profile/password', {
|
||||
templateUrl: 'public/app/features/profile/partials/password.html',
|
||||
templateUrl: 'public/app/features/org/partials/change_password.html',
|
||||
controller : 'ChangePasswordCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/profile/select-org', {
|
||||
templateUrl: 'public/app/features/profile/partials/select_org.html',
|
||||
templateUrl: 'public/app/features/org/partials/select_org.html',
|
||||
controller : 'SelectOrgCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
// ADMIN
|
||||
.when('/admin', {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'../core_module',
|
||||
'app/core/store',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, _, coreModule, store, config) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.service('contextSrv', function() {
|
||||
|
||||
function User() {
|
||||
if (config.bootData.user) {
|
||||
_.extend(this, config.bootData.user);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasRole = function(role) {
|
||||
return this.user.orgRole === role;
|
||||
};
|
||||
|
||||
this.setPinnedState = function(val) {
|
||||
this.pinned = val;
|
||||
store.set('grafana.sidemenu.pinned', val);
|
||||
};
|
||||
|
||||
this.toggleSideMenu = function() {
|
||||
this.sidemenu = !this.sidemenu;
|
||||
if (!this.sidemenu) {
|
||||
this.setPinnedState(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.pinned = store.getBool('grafana.sidemenu.pinned', false);
|
||||
if (this.pinned) {
|
||||
this.sidemenu = true;
|
||||
}
|
||||
|
||||
this.version = config.buildInfo.version;
|
||||
this.lightTheme = false;
|
||||
this.user = new User();
|
||||
this.isSignedIn = this.user.isSignedIn;
|
||||
this.isGrafanaAdmin = this.user.isGrafanaAdmin;
|
||||
this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import store from 'app/core/store';
|
||||
|
||||
export class User {
|
||||
isGrafanaAdmin: any;
|
||||
isSignedIn: any;
|
||||
orgRole: any;
|
||||
|
||||
constructor() {
|
||||
if (config.bootData.user) {
|
||||
_.extend(this, config.bootData.user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ContextSrv {
|
||||
pinned: any;
|
||||
version: any;
|
||||
user: User;
|
||||
isSignedIn: any;
|
||||
isGrafanaAdmin: any;
|
||||
isEditor: any;
|
||||
sidemenu: any;
|
||||
lightTheme: any;
|
||||
|
||||
constructor() {
|
||||
this.pinned = store.getBool('grafana.sidemenu.pinned', false);
|
||||
if (this.pinned) {
|
||||
this.sidemenu = true;
|
||||
}
|
||||
|
||||
if (!config.buildInfo) {
|
||||
config.buildInfo = {};
|
||||
}
|
||||
if (!config.bootData) {
|
||||
config.bootData = {user: {}, settings: {}};
|
||||
}
|
||||
|
||||
this.version = config.buildInfo.version;
|
||||
this.lightTheme = false;
|
||||
this.user = new User();
|
||||
this.isSignedIn = this.user.isSignedIn;
|
||||
this.isGrafanaAdmin = this.user.isGrafanaAdmin;
|
||||
this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
|
||||
}
|
||||
|
||||
hasRole(role) {
|
||||
return this.user.orgRole === role;
|
||||
}
|
||||
|
||||
setPinnedState(val) {
|
||||
this.pinned = val;
|
||||
store.set('grafana.sidemenu.pinned', val);
|
||||
}
|
||||
|
||||
toggleSideMenu() {
|
||||
this.sidemenu = !this.sidemenu;
|
||||
if (!this.sidemenu) {
|
||||
this.setPinnedState(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contextSrv = new ContextSrv();
|
||||
export {contextSrv};
|
||||
|
||||
coreModule.factory('contextSrv', function() {
|
||||
return contextSrv;
|
||||
});
|
||||
@@ -7,8 +7,5 @@ define([
|
||||
'./playlist/all',
|
||||
'./snapshot/all',
|
||||
'./panel/all',
|
||||
'./profile/profileCtrl',
|
||||
'./profile/changePasswordCtrl',
|
||||
'./profile/selectOrgCtrl',
|
||||
'./styleguide/styleguide',
|
||||
], function () {});
|
||||
|
||||
@@ -134,6 +134,10 @@ function (angular, $, config, moment) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.timezoneChanged = function() {
|
||||
$rootScope.$broadcast("refresh");
|
||||
};
|
||||
|
||||
$scope.formatDate = function(date) {
|
||||
return moment(date).format('MMM Do YYYY, h:mm:ss a');
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ function (angular, $, _, moment) {
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.factory('dashboardSrv', function() {
|
||||
module.factory('dashboardSrv', function(contextSrv) {
|
||||
|
||||
function DashboardModel (data, meta) {
|
||||
if (!data) {
|
||||
@@ -25,7 +25,7 @@ function (angular, $, _, moment) {
|
||||
this.originalTitle = this.title;
|
||||
this.tags = data.tags || [];
|
||||
this.style = data.style || "dark";
|
||||
this.timezone = data.timezone || 'browser';
|
||||
this.timezone = data.timezone || '';
|
||||
this.editable = data.editable !== false;
|
||||
this.hideControls = data.hideControls || false;
|
||||
this.sharedCrosshair = data.sharedCrosshair || false;
|
||||
@@ -208,6 +208,14 @@ function (angular, $, _, moment) {
|
||||
});
|
||||
};
|
||||
|
||||
p.isTimezoneUtc = function() {
|
||||
return this.getTimezone() === 'utc';
|
||||
};
|
||||
|
||||
p.getTimezone = function() {
|
||||
return this.timezone ? this.timezone : contextSrv.user.timezone;
|
||||
};
|
||||
|
||||
p._updateSchema = function(old) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="editJson();">View JSON</a></li>
|
||||
<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
|
||||
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
|
||||
<li ng-if="dashboard.id"><a class="pointer" ng-click="saveDashboardAsHome();">Set As Home</a></li>
|
||||
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Timezone</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f for f in ['browser','utc']"></select>
|
||||
<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,66 +62,67 @@
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 1">
|
||||
<h5 class="section-heading">Rows settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline" ng-repeat="row in dashboard.rows">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Title</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='row.title'></input>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Show title" checked="row.showTitle" switch-class="max-width-6"></gf-form-switch>
|
||||
<div ng-if="editor.index == 1">
|
||||
<h5 class="section-heading">Rows settings</h5>
|
||||
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn" ng-click="_.move(dashboard.rows,$index,$index-1)">
|
||||
<i ng-class="{'invisible': $first}" class="fa fa-arrow-up"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse gf-from-btn" ng-click="_.move(dashboard.rows,$index,$index+1)">
|
||||
<i ng-class="{'invisible': $last}" class="fa fa-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse gf-form-btn" click="dashboard.rows = _.without(dashboard.rows,row)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline" ng-repeat="row in dashboard.rows">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Title</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='row.title'></input>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Show title" checked="row.showTitle" switch-class="max-width-6"></gf-form-switch>
|
||||
|
||||
<div ng-if="editor.index == 2">
|
||||
<dash-links-editor></dash-links-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn" ng-click="_.move(dashboard.rows,$index,$index-1)">
|
||||
<i ng-class="{'invisible': $first}" class="fa fa-arrow-up"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse gf-from-btn" ng-click="_.move(dashboard.rows,$index,$index+1)">
|
||||
<i ng-class="{'invisible': $last}" class="fa fa-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse gf-form-btn" click="dashboard.rows = _.without(dashboard.rows,row)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 3">
|
||||
<gf-time-picker-settings dashboard="dashboard"></gf-time-picker-settings>
|
||||
</div>
|
||||
<div ng-if="editor.index == 2">
|
||||
<dash-links-editor></dash-links-editor>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 4">
|
||||
<h5 class="section-heading">Dashboard info</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Last updated at:</span>
|
||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.updated)}}</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Last updated by:</span>
|
||||
<span class="gf-form-label width-18">{{dashboardMeta.updatedBy}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Created at:</span>
|
||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.created)}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Created by:</span>
|
||||
<span class="gf-form-label width-18">{{dashboardMeta.createdBy}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Current version:</span>
|
||||
<span class="gf-form-label width-18">{{dashboardMeta.version}} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="editor.index == 3">
|
||||
<gf-time-picker-settings dashboard="dashboard"></gf-time-picker-settings>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 4">
|
||||
<h5 class="section-heading">Dashboard info</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Last updated at:</span>
|
||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.updated)}}</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Last updated by:</span>
|
||||
<span class="gf-form-label width-18">{{dashboardMeta.updatedBy}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Created at:</span>
|
||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.created)}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Created by:</span>
|
||||
<span class="gf-form-label width-18">{{dashboardMeta.createdBy}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Current version:</span>
|
||||
<span class="gf-form-label width-18">{{dashboardMeta.version}} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export class TimePickerCtrl {
|
||||
var time = angular.copy(this.timeSrv.timeRange());
|
||||
var timeRaw = angular.copy(this.timeSrv.timeRange(false));
|
||||
|
||||
if (this.dashboard.timezone === 'browser') {
|
||||
if (!this.dashboard.isTimezoneUtc()) {
|
||||
time.from.local();
|
||||
time.to.local();
|
||||
if (moment.isMoment(timeRaw.from)) {
|
||||
@@ -125,7 +125,7 @@ export class TimePickerCtrl {
|
||||
}
|
||||
|
||||
getAbsoluteMomentForTimezone(jsDate) {
|
||||
return this.dashboard.timezone === 'browser' ? moment(jsDate) : moment(jsDate).utc();
|
||||
return this.dashboard.isTimezoneUtc() ? moment(jsDate).utc() : moment(jsDate);
|
||||
}
|
||||
|
||||
setRelativeFilter(timespan) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
define([
|
||||
'./org_users_ctrl',
|
||||
'./profile_ctrl',
|
||||
'./org_users_ctrl',
|
||||
'./select_org_ctrl',
|
||||
'./change_password_ctrl',
|
||||
'./newOrgCtrl',
|
||||
'./userInviteCtrl',
|
||||
'./orgApiKeysCtrl',
|
||||
'./orgDetailsCtrl',
|
||||
'./prefs_control',
|
||||
], function () {});
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<prefs-control mode="org"></prefs-control>
|
||||
|
||||
<h3 class="page-heading">Address</h3>
|
||||
|
||||
|
||||
+12
-15
@@ -6,31 +6,28 @@
|
||||
<h1>Profile</h1>
|
||||
</div>
|
||||
|
||||
<form name="userForm" class="gf-form-group">
|
||||
<h3 class="page-heading">Preferences</h3>
|
||||
<form name="ctrl.userForm" class="gf-form-group">
|
||||
<h3 class="page-heading">Information</h3>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input class="gf-form-input max-width-21" type="text" required ng-model="user.name" >
|
||||
<span class="gf-form-label width-9">Name</span>
|
||||
<input class="gf-form-input max-width-21" type="text" required ng-model="ctrl.user.name" >
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Email</span>
|
||||
<input class="gf-form-input max-width-21" type="email" required ng-model="user.email">
|
||||
<span class="gf-form-label width-9">Email</span>
|
||||
<input class="gf-form-input max-width-21" type="email" required ng-model="ctrl.user.email">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Username</span>
|
||||
<input class="gf-form-input max-width-21" type="text" required ng-model="user.login">
|
||||
<span class="gf-form-label width-9">Username</span>
|
||||
<input class="gf-form-input max-width-21" type="text" required ng-model="ctrl.user.login">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">UI Theme</span>
|
||||
<select class="gf-form-input gf-size-auto" ng-model="user.theme" ng-options="f for f in ['dark', 'light']"></select>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="update()">Update</button>
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<prefs-control mode="user"></prefs-control>
|
||||
|
||||
<h3 class="page-heading">Password</h3>
|
||||
<div class="gf-form-group">
|
||||
<a href="profile/password" class="btn btn-inverse">Change Password</a>
|
||||
@@ -47,7 +44,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="org in orgs">
|
||||
<tr ng-repeat="org in ctrl.orgs">
|
||||
<td>{{org.name}}</td>
|
||||
<td>{{org.role}}</td>
|
||||
<td class="text-right">
|
||||
@@ -0,0 +1,98 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
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; }
|
||||
|
||||
var 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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var template = `
|
||||
<form name="ctrl.prefsForm" class="gf-form-group">
|
||||
<h3 class="page-heading">Preferences</h3>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">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-9">Home Dashboard</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-9">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()">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
export function prefsControlDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: PrefsControlCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
template: template,
|
||||
scope: {
|
||||
mode: "@"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('prefsControl', prefsControlDirective);
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import {coreModule} from 'app/core/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class ProfileCtrl {
|
||||
user: any;
|
||||
old_theme: any;
|
||||
orgs: any;
|
||||
userForm: any;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private backendSrv, private contextSrv, private $location) {
|
||||
this.getUser();
|
||||
this.getUserOrgs();
|
||||
}
|
||||
|
||||
getUser() {
|
||||
this.backendSrv.get('/api/user').then(user => {
|
||||
this.user = user;
|
||||
this.user.theme = user.theme || 'dark';
|
||||
});
|
||||
}
|
||||
|
||||
getUserOrgs() {
|
||||
this.backendSrv.get('/api/user/orgs').then(orgs => {
|
||||
this.orgs = orgs;
|
||||
});
|
||||
}
|
||||
|
||||
setUsingOrg(org) {
|
||||
this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
|
||||
window.location.href = config.appSubUrl + '/profile';
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.userForm.$valid) { return; }
|
||||
|
||||
this.backendSrv.put('/api/user/', this.user).then(() => {
|
||||
this.contextSrv.user.name = this.user.name || this.user.login;
|
||||
if (this.old_theme !== this.user.theme) {
|
||||
window.location.href = config.appSubUrl + this.$location.path();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
coreModule.controller('ProfileCtrl', ProfileCtrl);
|
||||
@@ -165,6 +165,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
|
||||
issueQueries(datasource) {
|
||||
this.updateTimeRange();
|
||||
this.datasource = datasource;
|
||||
|
||||
if (!this.panel.targets || this.panel.targets.length === 0) {
|
||||
return this.$q.when([]);
|
||||
@@ -251,7 +252,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
|
||||
addDataQuery(datasource) {
|
||||
var target = {
|
||||
datasource: datasource ? datasource.name : undefined
|
||||
};
|
||||
this.panel.targets.push(target);
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ProfileCtrl', function($scope, backendSrv, contextSrv, $location) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getUser();
|
||||
$scope.getUserOrgs();
|
||||
};
|
||||
|
||||
$scope.getUser = function() {
|
||||
backendSrv.get('/api/user').then(function(user) {
|
||||
$scope.user = user;
|
||||
$scope.user.theme = user.theme || 'dark';
|
||||
$scope.old_theme = $scope.user.theme;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getUserOrgs = function() {
|
||||
backendSrv.get('/api/user/orgs').then(function(orgs) {
|
||||
$scope.orgs = orgs;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setUsingOrg = function(org) {
|
||||
backendSrv.post('/api/user/using/' + org.orgId).then(function() {
|
||||
window.location.href = config.appSubUrl + '/profile';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if (!$scope.userForm.$valid) { return; }
|
||||
|
||||
backendSrv.put('/api/user/', $scope.user).then(function() {
|
||||
contextSrv.user.name = $scope.user.name || $scope.user.login;
|
||||
if ($scope.old_theme !== $scope.user.theme) {
|
||||
window.location.href = config.appSubUrl + $location.path();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
@@ -77,7 +77,7 @@ function (angular, _, queryDef) {
|
||||
switch($scope.agg.type) {
|
||||
case 'terms': {
|
||||
settings.order = settings.order || "asc";
|
||||
settings.size = settings.size || "0";
|
||||
settings.size = settings.size || "10";
|
||||
settings.orderBy = settings.orderBy || "_term";
|
||||
|
||||
if (settings.size !== '0') {
|
||||
|
||||
@@ -193,7 +193,7 @@ export class InfluxQueryCtrl extends QueryCtrl {
|
||||
|
||||
if (addTemplateVars) {
|
||||
for (let variable of this.templateSrv.variables) {
|
||||
segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
|
||||
segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# CloudWatch Datasource - Native Plugin
|
||||
# Prometheus Datasource - Native Plugin
|
||||
|
||||
Grafana ships with **built in** support for Prometheus, the open-source service monitoring system and time series database.
|
||||
|
||||
Read more about it here:
|
||||
|
||||
[http://docs.grafana.org/datasources/prometheus/](http://docs.grafana.org/datasources/prometheus/)
|
||||
[http://docs.grafana.org/datasources/prometheus/](http://docs.grafana.org/datasources/prometheus/)
|
||||
|
||||
@@ -279,7 +279,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
|
||||
var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
|
||||
|
||||
options.xaxis = {
|
||||
timezone: dashboard.timezone,
|
||||
timezone: dashboard.getTimezone(),
|
||||
show: panel['x-axis'],
|
||||
mode: "time",
|
||||
min: min,
|
||||
|
||||
@@ -157,7 +157,7 @@ function (angular, _, $) {
|
||||
}
|
||||
|
||||
var html = '<div class="graph-legend-series';
|
||||
if (series.yaxis === 2) { html += ' pull-right'; }
|
||||
if (series.yaxis === 2) { html += ' graph-legend-series--right-y'; }
|
||||
if (ctrl.hiddenSeries[series.alias]) { html += ' graph-legend-series-hidden'; }
|
||||
html += '" data-series-index="' + i + '">';
|
||||
html += '<div class="graph-legend-icon">';
|
||||
|
||||
@@ -53,7 +53,9 @@ describe('grafanaGraph', function() {
|
||||
},
|
||||
renderingCompleted: sinon.spy(),
|
||||
hiddenSeries: {},
|
||||
dashboard: {timezone: 'browser'},
|
||||
dashboard: {
|
||||
getTimezone: sinon.stub().returns('browser')
|
||||
},
|
||||
range: {
|
||||
from: moment([2015, 1, 1, 10]),
|
||||
to: moment([2015, 1, 1, 22]),
|
||||
|
||||
@@ -114,9 +114,13 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.table = transformDataToTable(this.dataRaw, this.panel);
|
||||
this.table.sort(this.panel.sort);
|
||||
this.render(this.table);
|
||||
return super.render(this.table);
|
||||
}
|
||||
|
||||
toggleColumnSort(col, colIndex) {
|
||||
@@ -130,7 +134,6 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
this.panel.sort.col = colIndex;
|
||||
this.panel.sort.desc = true;
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
@@ -155,7 +158,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
function appendTableRows(tbodyElem) {
|
||||
var renderer = new TableRenderer(panel, data, ctrl.dashboard.timezone);
|
||||
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc());
|
||||
tbodyElem.empty();
|
||||
tbodyElem.html(renderer.render(ctrl.pageIndex));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export class TableRenderer {
|
||||
formaters: any[];
|
||||
colorState: any;
|
||||
|
||||
constructor(private panel, private table, private timezone) {
|
||||
constructor(private panel, private table, private isUtc) {
|
||||
this.formaters = [];
|
||||
this.colorState = {};
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export class TableRenderer {
|
||||
return v => {
|
||||
if (_.isArray(v)) { v = v[0]; }
|
||||
var date = moment(v);
|
||||
if (this.timezone === 'utc') {
|
||||
if (this.isUtc) {
|
||||
date = date.utc();
|
||||
}
|
||||
return date.format(style.dateFormat);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
@import "base/grid";
|
||||
@import "base/font_awesome";
|
||||
@import "base/grafana_icons";
|
||||
@import "base/code";
|
||||
|
||||
// UTILS
|
||||
@import "utils/utils";
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// ---------------------
|
||||
@include media-breakpoint-down(sm) {
|
||||
div.panel {
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
width: 100% !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
.panel-margin {
|
||||
margin-right: 0;
|
||||
|
||||
@@ -11,28 +11,28 @@ pre {
|
||||
background-color: $code-tag-bg;
|
||||
color: $text-color;
|
||||
border: 1px solid darken($code-tag-bg, 15%);
|
||||
padding: 2px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// Inline code
|
||||
code {
|
||||
color: #d14;
|
||||
background-color: #f7f7f9;
|
||||
border: 1px solid #e1e1e8;
|
||||
color: $text-color;
|
||||
background-color: $code-tag-bg;
|
||||
border: 1px solid darken($code-tag-bg, 15%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Blocks of code
|
||||
pre {
|
||||
display: block;
|
||||
margin: 0 0 $line-height-base / 2;
|
||||
font-size: $font-size-base - 1; // 14px to 13px
|
||||
margin: 0 0 $line-height-base;
|
||||
line-height: $line-height-base;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
background-color: #f5f5f5;
|
||||
background-color: $code-tag-bg;
|
||||
|
||||
// Make prettyprint styles more spaced out for readability
|
||||
&.prettyprint {
|
||||
|
||||
@@ -66,12 +66,6 @@
|
||||
border: solid 0.08em #eeeeee;
|
||||
border-radius: .1em;
|
||||
}
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
.pull-left {
|
||||
float: left;
|
||||
}
|
||||
.fa.pull-left {
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
float: left;
|
||||
white-space: nowrap;
|
||||
padding-left: 10px;
|
||||
|
||||
&--right-y {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-legend-value {
|
||||
@@ -83,14 +87,22 @@
|
||||
.graph-legend-table {
|
||||
overflow-y: scroll;
|
||||
|
||||
.graph-legend-series { display: table-row;
|
||||
.graph-legend-series {
|
||||
display: table-row;
|
||||
float: none;
|
||||
padding-left: 0;
|
||||
&.pull-right {
|
||||
&--right-y {
|
||||
float: none;
|
||||
|
||||
.graph-legend-alias:after {
|
||||
content: '(right-y)';
|
||||
padding: 0 5px;
|
||||
color: $text-color-weak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
td, .graph-legend-alias, .graph-legend-icon, .graph-legend-value {
|
||||
float: none;
|
||||
display: table-cell;
|
||||
|
||||
@@ -7,6 +7,10 @@ define([
|
||||
var _dashboardSrv;
|
||||
|
||||
beforeEach(module('grafana.services'));
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('contextSrv', {
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function(dashboardSrv) {
|
||||
_dashboardSrv = dashboardSrv;
|
||||
|
||||
@@ -12,6 +12,11 @@ define([
|
||||
ctx.setup = function (setupFunc) {
|
||||
|
||||
beforeEach(module('grafana.services'));
|
||||
beforeEach(module(function($provide) {
|
||||
$provide.value('contextSrv', {
|
||||
user: { timezone: 'utc'}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) {
|
||||
ctx.dynamicDashboardSrv = dynamicDashboardSrv;
|
||||
@@ -45,10 +50,10 @@ define([
|
||||
value: ['se1', 'se2', 'se3']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -93,7 +98,7 @@ define([
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
|
||||
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -13,6 +13,6 @@
|
||||
"commit": "5a07c5107b4d24f41744a02b07717d55bad88e70"
|
||||
},
|
||||
"_source": "git://github.com/angular/bower-angular.git",
|
||||
"_target": "1.5.3",
|
||||
"_target": "~1.5.3",
|
||||
"_originalSource": "angular"
|
||||
}
|
||||
Reference in New Issue
Block a user