Merge branch 'master' of https://github.com/grafana/grafana into metadata

This commit is contained in:
utkarshcmu 2016-02-01 10:32:36 -08:00
commit 338afc80d5
284 changed files with 3875 additions and 234675 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ awsconfig
/dist
/emails/dist
/public_gen
/public/vendor/npm
/tmp
vendor/phantomjs/phantomjs

View File

@ -6,14 +6,15 @@
* **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
### Breaking changes
**Plugin API**: Both datasource and panel plugin api (and plugin.json schema) as been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
**InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
**KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
* **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
* **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
### Enhancements
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
* **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
### Bug fixes
* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)

View File

@ -18,11 +18,11 @@ The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlis
Click on "New Playlist" button to create a new playlist. Firstly, name your playlist and configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist.
You can search Dashboards by name (or use a regular expression), and add them to your Playlist. By default, your starred dashboards will appear as candidates for the Playlist.
You can search Dashboards by name (or use a regular expression), and add them to your Playlist. Or you could add tags which will include all the dashboards that belongs to a tag when the playlist start playing. By default, your starred dashboards will appear as candidates for the Playlist.
Be sure to click the "Add to dashboard" button next to the Dashboard name to add it to the Playlist. To remove a dashboard from the playlist click on "Remove[x]" button from the playlist.
Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
## Saving the playlist

View File

@ -59,7 +59,8 @@
},
"scripts": {
"test": "grunt test",
"coveralls": "grunt karma:coveralls && rm -rf ./coverage"
"coveralls": "grunt karma:coveralls && rm -rf ./coverage",
"postinstall": "grunt copy:node_modules"
},
"license": "Apache-2.0",
"dependencies": {

View File

@ -94,8 +94,15 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
ctx.JsonApiErr(500, "failed to get AppSettings.", err)
return
}
err = t.Execute(&contentBuf, query.Result.JsonData)
type templateData struct {
JsonData map[string]interface{}
SecureJsonData map[string]string
}
data := templateData{
JsonData: query.Result.JsonData,
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
}
err = t.Execute(&contentBuf, data)
if err != nil {
ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
return

View File

@ -103,5 +103,6 @@ func ProxyDataSourceRequest(c *middleware.Context) {
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
proxy.Transport = dataProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie")
}
}

View File

@ -123,6 +123,8 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
panels[panel.Id] = map[string]interface{}{
"module": panel.Module,
"name": panel.Name,
"id": panel.Id,
"info": panel.Info,
}
}

View File

@ -1,11 +1,8 @@
package api
import (
"errors"
"strconv"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
_ "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
)
@ -101,39 +98,6 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) {
return *itemQuery.Result, nil
}
func LoadPlaylistDashboards(id int64) ([]m.PlaylistDashboardDto, error) {
playlistItems, _ := LoadPlaylistItems(id)
dashboardIds := make([]int64, 0)
for _, i := range playlistItems {
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardIds = append(dashboardIds, dashboardId)
}
if len(dashboardIds) == 0 {
return make([]m.PlaylistDashboardDto, 0), nil
}
dashboardQuery := m.GetPlaylistDashboardsQuery{DashboardIds: dashboardIds}
if err := bus.Dispatch(&dashboardQuery); err != nil {
log.Warn("dashboardquery failed: %v", err)
return nil, errors.New("Playlist not found")
}
dtos := make([]m.PlaylistDashboardDto, 0)
for _, item := range *dashboardQuery.Result {
dtos = append(dtos, m.PlaylistDashboardDto{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
Uri: "db/" + item.Slug,
})
}
return dtos, nil
}
func GetPlaylistItems(c *middleware.Context) Response {
id := c.ParamsInt64(":id")
@ -147,9 +111,9 @@ func GetPlaylistItems(c *middleware.Context) Response {
}
func GetPlaylistDashboards(c *middleware.Context) Response {
id := c.ParamsInt64(":id")
playlistId := c.ParamsInt64(":id")
playlists, err := LoadPlaylistDashboards(id)
playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
if err != nil {
return ApiError(500, "Could not load dashboards", err)
}

88
pkg/api/playlist_play.go Normal file
View File

@ -0,0 +1,88 @@
package api
import (
"errors"
"strconv"
"github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
)
func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
result := make([]m.PlaylistDashboardDto, 0)
if len(dashboardByIds) > 0 {
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
if err := bus.Dispatch(&dashboardQuery); err != nil {
return result, errors.New("Playlist not found") //TODO: dont swallow error
}
for _, item := range *dashboardQuery.Result {
result = append(result, m.PlaylistDashboardDto{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
Uri: "db/" + item.Slug,
})
}
}
return result, nil
}
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
result := make([]m.PlaylistDashboardDto, 0)
if len(dashboardByTag) > 0 {
for _, tag := range dashboardByTag {
searchQuery := search.Query{
Title: "",
Tags: []string{tag},
UserId: userId,
Limit: 100,
IsStarred: false,
OrgId: orgId,
}
if err := bus.Dispatch(&searchQuery); err == nil {
for _, item := range searchQuery.Result {
result = append(result, m.PlaylistDashboardDto{
Id: item.Id,
Title: item.Title,
Uri: item.Uri,
})
}
}
}
}
return result
}
func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0)
dashboardByTag := make([]string, 0)
for _, i := range playlistItems {
if i.Type == "dashboard_by_id" {
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardByIds = append(dashboardByIds, dashboardId)
}
if i.Type == "dashboard_by_tag" {
dashboardByTag = append(dashboardByTag, i.Value)
}
}
result := make([]m.PlaylistDashboardDto, 0)
var k, _ = populateDashboardsById(dashboardByIds)
result = append(result, k...)
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
return result, nil
}

View File

@ -36,7 +36,6 @@ func newMacaron() *macaron.Macaron {
}
mapStatic(m, setting.StaticRootPath, "", "public")
mapStatic(m, setting.StaticRootPath, "app", "app")
mapStatic(m, setting.StaticRootPath, "css", "css")
mapStatic(m, setting.StaticRootPath, "img", "img")
mapStatic(m, setting.StaticRootPath, "fonts", "fonts")

View File

@ -46,8 +46,8 @@ var (
// ConsoleWriter implements LoggerInterface and writes messages to terminal.
type ConsoleWriter struct {
lg *log.Logger
Level int `json:"level"`
Formatting bool `json:"formatting"`
Level LogLevel `json:"level"`
Formatting bool `json:"formatting"`
}
// create ConsoleWriter returning as LoggerInterface.
@ -63,7 +63,7 @@ func (cw *ConsoleWriter) Init(config string) error {
return json.Unmarshal([]byte(config), cw)
}
func (cw *ConsoleWriter) WriteMsg(msg string, skip, level int) error {
func (cw *ConsoleWriter) WriteMsg(msg string, skip int, level LogLevel) error {
if cw.Level > level {
return nil
}
@ -82,11 +82,11 @@ func (_ *ConsoleWriter) Flush() {
func (_ *ConsoleWriter) Destroy() {
}
func printConsole(level int, msg string) {
func printConsole(level LogLevel, msg string) {
consoleWriter.WriteMsg(msg, 0, level)
}
func printfConsole(level int, format string, v ...interface{}) {
func printfConsole(level LogLevel, format string, v ...interface{}) {
consoleWriter.WriteMsg(fmt.Sprintf(format, v...), 0, level)
}

View File

@ -41,7 +41,7 @@ type FileLogWriter struct {
startLock sync.Mutex // Only one log can write to the file
Level int `json:"level"`
Level LogLevel `json:"level"`
}
// an *os.File writer with locker.
@ -132,7 +132,7 @@ func (w *FileLogWriter) docheck(size int) {
}
// write logger message into file.
func (w *FileLogWriter) WriteMsg(msg string, skip, level int) error {
func (w *FileLogWriter) WriteMsg(msg string, skip int, level LogLevel) error {
if level < w.Level {
return nil
}

View File

@ -99,7 +99,7 @@ func Close() {
type LogLevel int
const (
TRACE = iota
TRACE LogLevel = iota
DEBUG
INFO
WARN
@ -111,7 +111,7 @@ const (
// LoggerInterface represents behaviors of a logger provider.
type LoggerInterface interface {
Init(config string) error
WriteMsg(msg string, skip, level int) error
WriteMsg(msg string, skip int, level LogLevel) error
Destroy()
Flush()
}
@ -132,8 +132,9 @@ func Register(name string, log loggerType) {
}
type logMsg struct {
skip, level int
msg string
skip int
level LogLevel
msg string
}
// Logger is default logger in beego application.
@ -141,7 +142,7 @@ type logMsg struct {
type Logger struct {
adapter string
lock sync.Mutex
level int
level LogLevel
msg chan *logMsg
outputs map[string]LoggerInterface
quit chan bool
@ -188,10 +189,7 @@ func (l *Logger) DelLogger(adapter string) error {
return nil
}
func (l *Logger) writerMsg(skip, level int, msg string) error {
if l.level > level {
return nil
}
func (l *Logger) writerMsg(skip int, level LogLevel, msg string) error {
lm := &logMsg{
skip: skip,
level: level,
@ -266,36 +264,57 @@ func (l *Logger) Close() {
}
func (l *Logger) Trace(format string, v ...interface{}) {
if l.level > TRACE {
return
}
msg := fmt.Sprintf("[T] "+format, v...)
l.writerMsg(0, TRACE, msg)
}
func (l *Logger) Debug(format string, v ...interface{}) {
if l.level > DEBUG {
return
}
msg := fmt.Sprintf("[D] "+format, v...)
l.writerMsg(0, DEBUG, msg)
}
func (l *Logger) Info(format string, v ...interface{}) {
if l.level > INFO {
return
}
msg := fmt.Sprintf("[I] "+format, v...)
l.writerMsg(0, INFO, msg)
}
func (l *Logger) Warn(format string, v ...interface{}) {
if l.level > WARN {
return
}
msg := fmt.Sprintf("[W] "+format, v...)
l.writerMsg(0, WARN, msg)
}
func (l *Logger) Error(skip int, format string, v ...interface{}) {
if l.level > ERROR {
return
}
msg := fmt.Sprintf("[E] "+format, v...)
l.writerMsg(skip, ERROR, msg)
}
func (l *Logger) Critical(skip int, format string, v ...interface{}) {
if l.level > CRITICAL {
return
}
msg := fmt.Sprintf("[C] "+format, v...)
l.writerMsg(skip, CRITICAL, msg)
}
func (l *Logger) Fatal(skip int, format string, v ...interface{}) {
if l.level > FATAL {
return
}
msg := fmt.Sprintf("[F] "+format, v...)
l.writerMsg(skip, FATAL, msg)
l.Close()

View File

@ -39,7 +39,7 @@ func (sw *SyslogWriter) Init(config string) error {
return nil
}
func (sw *SyslogWriter) WriteMsg(msg string, skip, level int) error {
func (sw *SyslogWriter) WriteMsg(msg string, skip int, level LogLevel) error {
var err error
switch level {

View File

@ -3,6 +3,9 @@ package models
import (
"errors"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
@ -10,25 +13,37 @@ var (
)
type AppSettings struct {
Id int64
AppId string
OrgId int64
Enabled bool
Pinned bool
JsonData map[string]interface{}
Id int64
AppId string
OrgId int64
Enabled bool
Pinned bool
JsonData map[string]interface{}
SecureJsonData SecureJsonData
Created time.Time
Updated time.Time
}
type SecureJsonData map[string][]byte
func (s SecureJsonData) Decrypt() map[string]string {
decrypted := make(map[string]string)
for key, data := range s {
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
}
return decrypted
}
// ----------------------
// COMMANDS
// Also acts as api DTO
type UpdateAppSettingsCmd struct {
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
JsonData map[string]interface{} `json:"jsonData"`
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
JsonData map[string]interface{} `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
AppId string `json:"-"`
OrgId int64 `json:"-"`

View File

@ -150,3 +150,8 @@ type GetDashboardTagsQuery struct {
OrgId int64
Result []*DashboardTagCloudItem
}
type GetDashboardsQuery struct {
DashboardIds []int64
Result *[]Dashboard
}

View File

@ -76,9 +76,7 @@ type UpdatePlaylistCommand struct {
OrgId int64 `json:"-"`
Id int64 `json:"id" binding:"Required"`
Name string `json:"name" binding:"Required"`
Type string `json:"type"`
Interval string `json:"interval"`
Data []int64 `json:"data"`
Items []PlaylistItemDTO `json:"items"`
Result *PlaylistDTO
@ -86,9 +84,7 @@ type UpdatePlaylistCommand struct {
type CreatePlaylistCommand struct {
Name string `json:"name" binding:"Required"`
Type string `json:"type"`
Interval string `json:"interval"`
Data []int64 `json:"data"`
Items []PlaylistItemDTO `json:"items"`
OrgId int64 `json:"-"`
@ -121,8 +117,3 @@ type GetPlaylistItemsByIdQuery struct {
PlaylistId int64
Result *[]PlaylistItem
}
type GetPlaylistDashboardsQuery struct {
DashboardIds []int64
Result *PlaylistDashboards
}

View File

@ -5,6 +5,8 @@ import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func init() {
@ -40,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
sess.UseBool("enabled")
sess.UseBool("pinned")
if !exists {
// encrypt secureJsonData
secureJsonData := make(map[string][]byte)
for key, data := range cmd.SecureJsonData {
secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
app = m.AppSettings{
AppId: cmd.AppId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
Pinned: cmd.Pinned,
JsonData: cmd.JsonData,
Created: time.Now(),
Updated: time.Now(),
AppId: cmd.AppId,
OrgId: cmd.OrgId,
Enabled: cmd.Enabled,
Pinned: cmd.Pinned,
JsonData: cmd.JsonData,
SecureJsonData: secureJsonData,
Created: time.Now(),
Updated: time.Now(),
}
_, err = sess.Insert(&app)
return err
} else {
for key, data := range cmd.SecureJsonData {
app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
app.Updated = time.Now()
app.Enabled = cmd.Enabled
app.JsonData = cmd.JsonData

View File

@ -14,6 +14,7 @@ import (
func init() {
bus.AddHandler("sql", SaveDashboard)
bus.AddHandler("sql", GetDashboard)
bus.AddHandler("sql", GetDashboards)
bus.AddHandler("sql", DeleteDashboard)
bus.AddHandler("sql", SearchDashboards)
bus.AddHandler("sql", GetDashboardTags)
@ -223,3 +224,20 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
return nil
})
}
func GetDashboards(query *m.GetDashboardsQuery) error {
if len(query.DashboardIds) == 0 {
return m.ErrCommandValidationFailed
}
var dashboards = make([]m.Dashboard, 0)
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = &dashboards
if err != nil {
return err
}
return nil
}

View File

@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addAppSettingsMigration(mg *Migrator) {
appSettingsV1 := Table{
appSettingsV2 := Table{
Name: "app_settings",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) {
{Name: "enabled", Type: DB_Bool, Nullable: false},
{Name: "pinned", Type: DB_Bool, Nullable: false},
{Name: "json_data", Type: DB_Text, Nullable: true},
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
@ -21,8 +22,10 @@ func addAppSettingsMigration(mg *Migrator) {
},
}
mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1))
mg.AddMigration("Drop old table app_settings v1", NewDropTableMigration("app_settings"))
mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2))
//------- indexes ------------------
addTableIndicesMigrations(mg, "v3", appSettingsV1)
addTableIndicesMigrations(mg, "v3", appSettingsV2)
}

View File

@ -15,7 +15,6 @@ func init() {
bus.AddHandler("sql", DeletePlaylist)
bus.AddHandler("sql", SearchPlaylists)
bus.AddHandler("sql", GetPlaylist)
bus.AddHandler("sql", GetPlaylistDashboards)
bus.AddHandler("sql", GetPlaylistItem)
}
@ -162,20 +161,3 @@ func GetPlaylistItem(query *m.GetPlaylistItemsByIdQuery) error {
return err
}
func GetPlaylistDashboards(query *m.GetPlaylistDashboardsQuery) error {
if len(query.DashboardIds) == 0 {
return m.ErrCommandValidationFailed
}
var dashboards = make(m.PlaylistDashboards, 0)
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = &dashboards
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,44 @@
package sqlstore
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestPlaylistDataAccess(t *testing.T) {
Convey("Testing Playlist data access", t, func() {
InitTestDB(t)
Convey("Can create playlist", func() {
items := []m.PlaylistItemDTO{
{Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "3", Type: "dashboard_by_id"},
}
cmd := m.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items}
err := CreatePlaylist(&cmd)
So(err, ShouldBeNil)
Convey("can update playlist", func() {
items := []m.PlaylistItemDTO{
{Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"},
{Title: "Backend response times", Value: "2", Type: "dashboard_by_id"},
}
query := m.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, Id: 1, Interval: "10s", Items: items}
err = UpdatePlaylist(&query)
So(err, ShouldBeNil)
Convey("can remove playlist", func() {
query := m.DeletePlaylistCommand{Id: 1}
err = DeletePlaylist(&query)
So(err, ShouldBeNil)
})
})
})
})
}

View File

@ -71,7 +71,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
FROM ` + dialect.Quote("dashboard_snapshot") + `
) AS db_snapshot_count,
(
SELECT COUNT(*)
SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
FROM ` + dialect.Quote("dashboard_tag") + `
) AS db_tag_count,
(
@ -83,7 +83,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
FROM ` + dialect.Quote("playlist") + `
) AS playlist_count,
(
SELECT COUNT (DISTINCT ` + dialect.Quote("dashboard_id") + ` )
SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
FROM ` + dialect.Quote("star") + `
) AS starred_db_count,
(

66
pkg/util/encryption.go Normal file
View File

@ -0,0 +1,66 @@
package util
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"io"
"github.com/grafana/grafana/pkg/log"
)
const saltLength = 8
func Decrypt(payload []byte, secret string) []byte {
salt := payload[:saltLength]
key := encryptionKeyToBytes(secret, string(salt))
block, err := aes.NewCipher(key)
if err != nil {
log.Fatal(4, err.Error())
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
if len(payload) < aes.BlockSize {
log.Fatal(4, "payload too short")
}
iv := payload[saltLength : saltLength+aes.BlockSize]
payload = payload[saltLength+aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
// XORKeyStream can work in-place if the two arguments are the same.
stream.XORKeyStream(payload, payload)
return payload
}
func Encrypt(payload []byte, secret string) []byte {
salt := GetRandomString(saltLength)
key := encryptionKeyToBytes(secret, salt)
block, err := aes.NewCipher(key)
if err != nil {
log.Fatal(4, err.Error())
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload))
copy(ciphertext[:saltLength], []byte(salt))
iv := ciphertext[saltLength : saltLength+aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
log.Fatal(4, err.Error())
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload)
return ciphertext
}
// Key needs to be 32bytes
func encryptionKeyToBytes(secret, salt string) []byte {
return PBKDF2([]byte(secret), []byte(salt), 10000, 32, sha256.New)
}

View File

@ -0,0 +1,27 @@
package util
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestEncryption(t *testing.T) {
Convey("When getting encryption key", t, func() {
key := encryptionKeyToBytes("secret", "salt")
So(len(key), ShouldEqual, 32)
key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt")
So(len(key), ShouldEqual, 32)
})
Convey("When decrypting basic payload", t, func() {
encrypted := Encrypt([]byte("grafana"), "1234")
decrypted := Decrypt(encrypted, "1234")
So(string(decrypted), ShouldEqual, "grafana")
})
}

View File

@ -27,6 +27,11 @@ func (r *UrlQueryReader) Get(name string, def string) string {
func JoinUrlFragments(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
if len(b) == 0 {
return a
}
switch {
case aslash && bslash:
return a + b[1:]

46
pkg/util/url_test.go Normal file
View File

@ -0,0 +1,46 @@
package util
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestUrl(t *testing.T) {
Convey("When joining two urls where right hand side is empty", t, func() {
result := JoinUrlFragments("http://localhost:8080", "")
So(result, ShouldEqual, "http://localhost:8080")
})
Convey("When joining two urls where right hand side is empty and lefthand side has a trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080/", "")
So(result, ShouldEqual, "http://localhost:8080/")
})
Convey("When joining two urls where neither has a trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080", "api")
So(result, ShouldEqual, "http://localhost:8080/api")
})
Convey("When joining two urls where lefthand side has a trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080/", "api")
So(result, ShouldEqual, "http://localhost:8080/api")
})
Convey("When joining two urls where righthand side has preceding slash", t, func() {
result := JoinUrlFragments("http://localhost:8080", "/api")
So(result, ShouldEqual, "http://localhost:8080/api")
})
Convey("When joining two urls where righthand side has trailing slash", t, func() {
result := JoinUrlFragments("http://localhost:8080", "api/")
So(result, ShouldEqual, "http://localhost:8080/api/")
})
}

View File

@ -5,7 +5,7 @@ import store from 'app/core/store';
import _ from 'lodash';
import angular from 'angular';
import $ from 'jquery';
import coreModule from '../core_module';
import coreModule from 'app/core/core_module';
export class GrafanaCtrl {
@ -150,6 +150,9 @@ export function grafanaAppDirective(playlistSrv) {
scope.$watch('contextSrv.sidemenu', newVal => {
if (newVal !== undefined) {
elem.toggleClass('sidemenu-open', scope.contextSrv.sidemenu);
if (!newVal) {
scope.contextSrv.setPinnedState(false);
}
}
if (scope.contextSrv.sidemenu) {
ignoreSideMenuHide = true;
@ -159,6 +162,12 @@ export function grafanaAppDirective(playlistSrv) {
}
});
scope.$watch('contextSrv.pinned', newVal => {
if (newVal !== undefined) {
elem.toggleClass('sidemenu-pinned', newVal);
}
});
// tooltip removal fix
scope.$on("$routeChangeSuccess", function() {
$("#tooltip, .tooltip").remove();
@ -182,7 +191,7 @@ export function grafanaAppDirective(playlistSrv) {
}
}
// hide sidemenu
if (!ignoreSideMenuHide && elem.find('.sidemenu').length > 0) {
if (!ignoreSideMenuHide && !scope.contextSrv.pinned && elem.find('.sidemenu').length > 0) {
if (target.parents('.sidemenu').length === 0) {
scope.$apply(() => scope.contextSrv.toggleSideMenu());
}

View File

@ -1,10 +1,11 @@
<div class="navbar navbar-static-top">
<div class="navbar">
<div class="navbar-inner"><div class="container-fluid">
<div class="top-nav-btn top-nav-menu-btn">
<a class="pointer" ng-click="ctrl.contextSrv.toggleSideMenu()">
<span class="top-nav-logo-background">
<img class="logo-icon" src="img/fav32.png"></img>
<img class="logo-icon" src="img/grafana_icon.svg"></img>
</span>
<i class="icon-gf icon-gf-grafana_wordmark"></i>
<i class="fa fa-caret-down"></i>
</a>
</div>

View File

@ -14,7 +14,7 @@ export class NavbarCtrl {
export function navbarDirective() {
return {
restrict: 'E',
templateUrl: 'app/core/components/navbar/navbar.html',
templateUrl: 'public/app/core/components/navbar/navbar.html',
controller: NavbarCtrl,
bindToController: true,
controllerAs: 'ctrl',

View File

@ -137,7 +137,7 @@ export class SearchCtrl {
export function searchDirective() {
return {
restrict: 'E',
templateUrl: 'app/core/components/search/search.html',
templateUrl: 'public/app/core/components/search/search.html',
controller: SearchCtrl,
bindToController: true,
controllerAs: 'ctrl',

View File

@ -62,5 +62,11 @@
</a>
</li>
<li>
<a class="sidemenu-item" target="_self" ng-hide="ctrl.contextSrv.pinned" ng-click="ctrl.contextSrv.setPinnedState(true)">
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-thumb-tack"></i></span>
<span class="sidemenu-item-text">Pin</span>
</a>
</li>
</ul>

View File

@ -22,8 +22,12 @@ export class SideMenuCtrl {
this.appSubUrl = config.appSubUrl;
this.showSignout = this.contextSrv.isSignedIn && !config['authProxyEnabled'];
this.updateMenu();
this.$scope.$on('$routeChangeSuccess', () => {
this.contextSrv.sidemenu = false;
this.updateMenu();
if (!this.contextSrv.pinned) {
this.contextSrv.sidemenu = false;
}
});
}
@ -83,11 +87,11 @@ export class SideMenuCtrl {
this.switchOrg(org.orgId);
}
});
if (config.allowOrgCreate) {
this.orgMenu.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
}
});
if (config.allowOrgCreate) {
this.orgMenu.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
}
});
}
@ -108,19 +112,19 @@ export class SideMenuCtrl {
});
this.mainLinks.push({
text: "Grafana stats",
text: "Stats",
icon: "fa fa-fw fa-bar-chart",
url: this.getUrl("/admin/stats"),
});
this.mainLinks.push({
text: "Global Users",
text: "Users",
icon: "fa fa-fw fa-user",
url: this.getUrl("/admin/users"),
});
this.mainLinks.push({
text: "Global Orgs",
text: "Organizations",
icon: "fa fa-fw fa-users",
url: this.getUrl("/admin/orgs"),
});
@ -144,7 +148,7 @@ export class SideMenuCtrl {
export function sideMenuDirective() {
return {
restrict: 'E',
templateUrl: 'app/core/components/sidemenu/sidemenu.html',
templateUrl: 'public/app/core/components/sidemenu/sidemenu.html',
controller: SideMenuCtrl,
bindToController: true,
controllerAs: 'ctrl',

View File

@ -15,7 +15,6 @@ import "./directives/ng_model_on_blur";
import "./directives/password_strenght";
import "./directives/spectrum_picker";
import "./directives/tags";
import "./directives/topnav";
import "./directives/value_select_dropdown";
import "./directives/give_focus";
import './jquery_extended';

View File

@ -6,9 +6,9 @@ function ($, coreModule) {
'use strict';
var editViewMap = {
'settings': { src: 'app/features/dashboard/partials/settings.html', title: "Settings" },
'annotations': { src: 'app/features/annotations/partials/editor.html', title: "Annotations" },
'templating': { src: 'app/features/templating/partials/editor.html', title: "Templating" }
'settings': { src: 'public/app/features/dashboard/partials/settings.html', title: "Settings" },
'annotations': { src: 'public/app/features/annotations/partials/editor.html', title: "Annotations" },
'templating': { src: 'public/app/features/templating/partials/editor.html', title: "Templating" }
};
coreModule.default.directive('dashEditorLink', function($timeout) {

View File

@ -1,50 +0,0 @@
define([
'../core_module',
],
function (coreModule) {
'use strict';
coreModule.default.directive('topnav', function($rootScope, contextSrv) {
return {
restrict: 'E',
transclude: true,
scope: {
title: "@",
section: "@",
titleUrl: "@",
subnav: "=",
},
template:
'<div class="navbar navbar-static-top"><div class="navbar-inner"><div class="container-fluid">' +
'<div class="top-nav">' +
'<div class="top-nav-btn top-nav-menu-btn">' +
'<a class="pointer" ng-click="contextSrv.toggleSideMenu()">' +
'<span class="top-nav-logo-background">' +
'<img class="logo-icon" src="img/fav32.png"></img>' +
'</span>' +
'<i class="fa fa-caret-down"></i>' +
'</a>' +
'</div>' +
'<span class="icon-circle top-nav-icon">' +
'<i ng-class="icon"></i>' +
'</span>' +
'<span ng-show="section">' +
'<span class="top-nav-title">{{section}}</span>' +
'<i class="top-nav-breadcrumb-icon fa fa-angle-right"></i>' +
'</span>' +
'<a ng-href="{{titleUrl}}" class="top-nav-title">' +
'{{title}}' +
'</a>' +
'<i ng-show="subnav" class="top-nav-breadcrumb-icon fa fa-angle-right"></i>' +
'</div><div ng-transclude></div></div></div></div>',
link: function(scope, elem, attrs) {
scope.icon = attrs.icon;
scope.contextSrv = contextSrv;
}
};
});
});

View File

@ -227,7 +227,7 @@ function (angular, _, coreModule) {
coreModule.default.directive('valueSelectDropdown', function($compile, $window, $timeout, $rootScope) {
return {
scope: { variable: "=", onUpdated: "&", getValuesForTag: "&" },
templateUrl: 'app/partials/valueSelectDropdown.html',
templateUrl: 'public/app/partials/valueSelectDropdown.html',
controller: 'ValueSelectDropdownCtrl',
controllerAs: 'vm',
bindToController: true,

View File

@ -59,11 +59,14 @@ coreModule.filter('noXml', function() {
coreModule.filter('interpolateTemplateVars', function (templateSrv) {
var filterFunc: any = function(text, scope) {
if (scope.panel) {
return templateSrv.replaceWithText(text, scope.panel.scopedVars);
var scopedVars;
if (scope.ctrl && scope.ctrl.panel) {
scopedVars = scope.ctrl.panel.scopedVars;
} else {
return templateSrv.replaceWithText(text, scope.row.scopedVars);
scopedVars = scope.row.scopedVars;
}
return templateSrv.replaceWithText(text, scopedVars);
};
filterFunc.$stateful = true;

View File

@ -14,146 +14,146 @@ define([
$routeProvider
.when('/', {
templateUrl: 'app/partials/dashboard.html',
templateUrl: 'public/app/partials/dashboard.html',
controller : 'LoadDashboardCtrl',
reloadOnSearch: false,
})
.when('/dashboard/:type/:slug', {
templateUrl: 'app/partials/dashboard.html',
templateUrl: 'public/app/partials/dashboard.html',
controller : 'LoadDashboardCtrl',
reloadOnSearch: false,
})
.when('/dashboard-solo/:type/:slug', {
templateUrl: 'app/features/panel/partials/soloPanel.html',
templateUrl: 'public/app/features/panel/partials/soloPanel.html',
controller : 'SoloPanelCtrl',
})
.when('/dashboard-import/:file', {
templateUrl: 'app/partials/dashboard.html',
templateUrl: 'public/app/partials/dashboard.html',
controller : 'DashFromImportCtrl',
reloadOnSearch: false,
})
.when('/dashboard/new', {
templateUrl: 'app/partials/dashboard.html',
templateUrl: 'public/app/partials/dashboard.html',
controller : 'NewDashboardCtrl',
reloadOnSearch: false,
})
.when('/import/dashboard', {
templateUrl: 'app/features/dashboard/partials/import.html',
templateUrl: 'public/app/features/dashboard/partials/import.html',
controller : 'DashboardImportCtrl',
})
.when('/datasources', {
templateUrl: 'app/features/datasources/partials/list.html',
templateUrl: 'public/app/features/datasources/partials/list.html',
controller : 'DataSourcesCtrl',
resolve: loadOrgBundle,
})
.when('/datasources/edit/:id', {
templateUrl: 'app/features/datasources/partials/edit.html',
templateUrl: 'public/app/features/datasources/partials/edit.html',
controller : 'DataSourceEditCtrl',
resolve: loadOrgBundle,
})
.when('/datasources/new', {
templateUrl: 'app/features/datasources/partials/edit.html',
templateUrl: 'public/app/features/datasources/partials/edit.html',
controller : 'DataSourceEditCtrl',
resolve: loadOrgBundle,
})
.when('/org', {
templateUrl: 'app/features/org/partials/orgDetails.html',
templateUrl: 'public/app/features/org/partials/orgDetails.html',
controller : 'OrgDetailsCtrl',
resolve: loadOrgBundle,
})
.when('/org/new', {
templateUrl: 'app/features/org/partials/newOrg.html',
templateUrl: 'public/app/features/org/partials/newOrg.html',
controller : 'NewOrgCtrl',
resolve: loadOrgBundle,
})
.when('/org/users', {
templateUrl: 'app/features/org/partials/orgUsers.html',
templateUrl: 'public/app/features/org/partials/orgUsers.html',
controller : 'OrgUsersCtrl',
resolve: loadOrgBundle,
})
.when('/org/apikeys', {
templateUrl: 'app/features/org/partials/orgApiKeys.html',
templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
controller : 'OrgApiKeysCtrl',
resolve: loadOrgBundle,
})
.when('/profile', {
templateUrl: 'app/features/profile/partials/profile.html',
templateUrl: 'public/app/features/profile/partials/profile.html',
controller : 'ProfileCtrl',
})
.when('/profile/password', {
templateUrl: 'app/features/profile/partials/password.html',
templateUrl: 'public/app/features/profile/partials/password.html',
controller : 'ChangePasswordCtrl',
})
.when('/profile/select-org', {
templateUrl: 'app/features/profile/partials/select_org.html',
templateUrl: 'public/app/features/profile/partials/select_org.html',
controller : 'SelectOrgCtrl',
})
.when('/admin/settings', {
templateUrl: 'app/features/admin/partials/settings.html',
templateUrl: 'public/app/features/admin/partials/settings.html',
controller : 'AdminSettingsCtrl',
})
.when('/admin/users', {
templateUrl: 'app/features/admin/partials/users.html',
templateUrl: 'public/app/features/admin/partials/users.html',
controller : 'AdminListUsersCtrl',
})
.when('/admin/users/create', {
templateUrl: 'app/features/admin/partials/new_user.html',
templateUrl: 'public/app/features/admin/partials/new_user.html',
controller : 'AdminEditUserCtrl',
})
.when('/admin/users/edit/:id', {
templateUrl: 'app/features/admin/partials/edit_user.html',
templateUrl: 'public/app/features/admin/partials/edit_user.html',
controller : 'AdminEditUserCtrl',
})
.when('/admin/orgs', {
templateUrl: 'app/features/admin/partials/orgs.html',
templateUrl: 'public/app/features/admin/partials/orgs.html',
controller : 'AdminListOrgsCtrl',
})
.when('/admin/orgs/edit/:id', {
templateUrl: 'app/features/admin/partials/edit_org.html',
templateUrl: 'public/app/features/admin/partials/edit_org.html',
controller : 'AdminEditOrgCtrl',
})
.when('/admin/stats', {
templateUrl: 'app/features/admin/partials/stats.html',
templateUrl: 'public/app/features/admin/partials/stats.html',
controller : 'AdminStatsCtrl',
controllerAs: 'ctrl',
})
.when('/login', {
templateUrl: 'app/partials/login.html',
templateUrl: 'public/app/partials/login.html',
controller : 'LoginCtrl',
})
.when('/invite/:code', {
templateUrl: 'app/partials/signup_invited.html',
templateUrl: 'public/app/partials/signup_invited.html',
controller : 'InvitedCtrl',
})
.when('/signup', {
templateUrl: 'app/partials/signup_step2.html',
templateUrl: 'public/app/partials/signup_step2.html',
controller : 'SignUpCtrl',
})
.when('/user/password/send-reset-email', {
templateUrl: 'app/partials/reset_password.html',
templateUrl: 'public/app/partials/reset_password.html',
controller : 'ResetPasswordCtrl',
})
.when('/user/password/reset', {
templateUrl: 'app/partials/reset_password.html',
templateUrl: 'public/app/partials/reset_password.html',
controller : 'ResetPasswordCtrl',
})
.when('/apps', {
templateUrl: 'app/features/apps/partials/list.html',
templateUrl: 'public/app/features/apps/partials/list.html',
controller: 'AppListCtrl',
controllerAs: 'ctrl',
resolve: loadAppsBundle,
})
.when('/apps/edit/:appId', {
templateUrl: 'app/features/apps/partials/edit.html',
templateUrl: 'public/app/features/apps/partials/edit.html',
controller: 'AppEditCtrl',
controllerAs: 'ctrl',
resolve: loadAppsBundle,
})
.when('/global-alerts', {
templateUrl: 'app/features/dashboard/partials/globalAlerts.html',
templateUrl: 'public/app/features/dashboard/partials/globalAlerts.html',
})
.otherwise({
templateUrl: 'app/partials/error.html',
templateUrl: 'public/app/partials/error.html',
controller: 'ErrorCtrl'
});
});

View File

@ -72,7 +72,7 @@ function (angular, _, coreModule) {
scope.noText = payload.noText || "Cancel";
var confirmModal = $modal({
template: './app/partials/confirm_modal.html',
template: 'public/app/partials/confirm_modal.html',
persist: false,
modalClass: 'modal-no-header confirm-modal',
show: false,

View File

@ -20,10 +20,23 @@ function (angular, _, coreModule, store, config) {
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();

View File

@ -1,9 +1,8 @@
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
<navbar icon="fa fa-fw fa-user" title="Organizations" title-url="admin/orgs" subnav="true">
<ul class="nav">
<li><a href="admin/orgs">List</a></li>
<li class="active"><a href="admin/orgs/edit/{{org.id}}">Edit Org</a></li>
<li class="active"><a href="admin/orgs/edit/{{org.id}}">{{org.name}}</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">

View File

@ -1,10 +1,8 @@
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users" subnav="true">
<ul class="nav">
<li><a href="admin/users">Users</a></li>
<li><a href="admin/users/create">Create user</a></li>
<li class="active"><a href="admin/users/edit/{{user_id}}">Edit user</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">

View File

@ -1,14 +1,13 @@
<topnav icon="fa fa-fw fa-cogs" title="Global Users" subnav="true">
<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users" subnav="true">
<ul class="nav">
<li><a href="admin/users">Users</a></li>
<li class="active"><a href="admin/users/create">Create user</a></li>
<li class="active"><a href="admin/users/create">Add user</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">
<h2>
Create a new user
Add new user
</h2>
<form name="userForm">

View File

@ -1,8 +1,5 @@
<topnav icon="fa fa-fw fa-users" title="Global Orgs" subnav="true">
<ul class="nav">
<li class="active"><a href="admin/orgs">List</a></li>
</ul>
</topnav>
<navbar icon="fa fa-fw fa-users" title="Organizations">
</navbar>
<div class="page-container">
<div class="page-wide">

View File

@ -1,5 +1,5 @@
<topnav icon="fa fa-fw fa-info" title="System info">
</topnav>
<navbar icon="fa fa-fw fa-info" title="System info">
</navbar>
<div class="page-container">
<div class="page">

View File

@ -1,8 +1,5 @@
<topnav icon="fa fa-fw fa-bar-chart" title="Grafana stats" subnav="true">
<ul class="nav">
<li class="active"><a href="admin/stats">Overview</a></li>
</ul>
</topnav>
<navbar icon="fa fa-fw fa-bar-chart" title="Stats">
</navbar>
<div class="page-container">
<div class="page-wide" ng-init="ctrl.init()">

View File

@ -1,15 +1,14 @@
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
<ul class="nav">
<li class="active"><a href="admin/users">List</a></li>
<li><a href="admin/users/create">Create user</a></li>
</ul>
</topnav>
<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users">
</navbar>
<div class="page-container">
<div class="page-wide">
<h1>
Users
</h1>
<a class="btn btn-inverse pull-right" href="admin/users/create">
<i class="fa fa-plus"></i>
Add new user
</a>
<h1>Users</h1>
<table class="filter-table form-inline">
<thead>

View File

@ -31,7 +31,7 @@ function (angular, _, $) {
};
$scope.datasourceChanged = function() {
datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
$scope.currentDatasource = ds;
$scope.currentAnnotation.datasource = ds.name;
});

View File

@ -33,9 +33,9 @@
<i class="fa fa-remove"></i>
</button>
</div>
<div class="gf-box-body">
<div class="editor-row row" ng-if="mode === 'list'">
<div class="span6">
<div ng-if="annotations.length === 0">

View File

@ -9,7 +9,7 @@ function annotationsQueryEditor(dynamicDirectiveSrv) {
annotation: "=",
datasource: "="
},
watchPath: "datasource.type",
watchPath: "annotation.datasource",
directive: scope => {
return System.import(scope.datasource.meta.module).then(function(dsModule) {
return {

View File

@ -24,6 +24,7 @@ export class AppEditCtrl {
enabled: this.appModel.enabled,
pinned: this.appModel.pinned,
jsonData: this.appModel.jsonData,
secureJsonData: this.appModel.secureJsonData,
}, options);
this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() {

View File

@ -1,8 +1,5 @@
<topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true">
<ul class="nav">
<li class="active" ><a href="org/apps">Overview</a></li>
</ul>
</topnav>
<navbar title="Apps" icon="fa fa-fw fa-cubes">
</navbar>
<div class="page-container">
<div class="page-wide" ng-init="ctrl.init()">

View File

@ -102,7 +102,7 @@ function (angular, $, config, moment) {
var editScope = $rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
$scope.appEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
};
$scope.onDrop = function(panelId, row, dropTarget) {

View File

@ -10,7 +10,7 @@
<div class="top-nav-snapshot-title" ng-if="dashboardMeta.isSnapshot">
<a class="pointer" bs-tooltip="titleTooltip" data-placement="bottom">
<i class="gf-icon gf-icon-snap-multi"></i>
<i class="icon-gf icon-gf-snapshot"></i>
<span class="dashboard-title">
{{dashboard.title}}
<em class="small">&nbsp;&nbsp;(snapshot)</em>
@ -24,8 +24,20 @@
<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
</a>
</li>
<li ng-show="dashboardMeta.canShare">
<a class="pointer" ng-click="shareDashboard()" bs-tooltip="'Share dashboard'" data-placement="bottom"><i class="fa fa-share-square-o"></i></a>
<li ng-show="dashboardMeta.canShare" class="dropdown">
<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
<ul class="dropdown-menu">
<li ng-if="dashboardMeta.canEdit">
<a class="pointer" ng-click="shareDashboard(0)">
<i class="fa fa-link"></i> Link to Dashboard
</a>
</li>
<li ng-if="dashboardMeta.canEdit">
<a class="pointer" ng-click="shareDashboard(1)">
<i class="icon-gf icon-gf-snapshot"></i>Snapshot sharing
</a>
</li>
</ul>
</li>
<li ng-show="dashboardMeta.canSave">
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>

View File

@ -42,10 +42,13 @@ export class DashNavCtrl {
}
};
$scope.shareDashboard = function() {
$scope.shareDashboard = function(tabIndex) {
var modalScope = $scope.$new();
modalScope.tabIndex = tabIndex;
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/shareModal.html',
scope: $scope.$new(),
src: 'public/app/features/dashboard/partials/shareModal.html',
scope: modalScope
});
};
@ -149,7 +152,7 @@ export class DashNavCtrl {
newScope.clone.hideControls = false;
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/saveDashboardAs.html',
src: 'public/app/features/dashboard/partials/saveDashboardAs.html',
scope: newScope,
});
};
@ -189,7 +192,7 @@ export class DashNavCtrl {
export function dashNavDirective() {
return {
restrict: 'E',
templateUrl: 'app/features/dashboard/dashnav/dashnav.html',
templateUrl: 'public/app/features/dashboard/dashnav/dashnav.html',
controller: DashNavCtrl,
transclude: true,
};

View File

@ -21,7 +21,7 @@ function(angular, $) {
helpModalScope = $rootScope.$new();
var helpModal = $modal({
template: './app/partials/help_modal.html',
template: 'public/app/partials/help_modal.html',
persist: false,
show: false,
scope: helpModalScope,

View File

@ -1,8 +1,8 @@
<topnav icon="fa fa-th-large" title="Dashboards" subnav="true">
<navbar icon="fa fa-th-large" title="Dashboards" subnav="true">
<ul class="nav">
<li class="active"><a href="import/dashboard">Import</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">
@ -59,7 +59,7 @@
</section>
</div>
<div ng-include="'app/features/dashboard/partials/graphiteImport.html'"></div>
<div ng-include="'public/app/features/dashboard/partials/graphiteImport.html'"></div>
</div>
</div>

View File

@ -25,7 +25,7 @@
Title
</li>
<li>
<input type="text" class="input-xlarge tight-form-input" ng-model='dashboard.title'></input>
<input type="text" class="input-large tight-form-input" ng-model='dashboard.title'></input>
</li>
<li class="tight-form-item">
Tags

View File

@ -89,7 +89,7 @@
<script type="text/ng-template" id="shareLink.html">
<div class="share-modal-big-icon">
<i class="fa fa-external-link"></i>
<i class="fa fa-link"></i>
</div>
<div ng-include src="'shareLinkOptions.html'"></div>
@ -110,7 +110,7 @@
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
<div class="share-modal-big-icon">
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
<i ng-if="!loading" class="gf-icon gf-icon-snap-multi"></i>
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
</div>
<div class="share-snapshot-header" ng-if="step === 1">

View File

@ -116,36 +116,7 @@ function (angular, _, config) {
$scope.$broadcast('render');
};
$scope.removePanel = function(panel) {
$scope.appEvent('confirm-modal', {
title: 'Are you sure you want to remove this panel?',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: function() {
$scope.row.panels = _.without($scope.row.panels, panel);
}
});
};
$scope.updatePanelSpan = function(panel, span) {
panel.span = Math.min(Math.max(Math.floor(panel.span + span), 1), 12);
};
$scope.replacePanel = function(newPanel, oldPanel) {
var row = $scope.row;
var index = _.indexOf(row.panels, oldPanel);
row.panels.splice(index, 1);
// adding it back needs to be done in next digest
$timeout(function() {
newPanel.id = oldPanel.id;
newPanel.span = oldPanel.span;
row.panels.splice(index, 0, newPanel);
});
};
$scope.init();
});
module.directive('rowHeight', function() {

View File

@ -12,7 +12,7 @@ function (angular, _, require, config) {
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
$scope.editor = { index: 0 };
$scope.editor = { index: $scope.tabIndex || 0};
$scope.init = function() {
$scope.modeSharePanel = $scope.panel ? true : false;
@ -26,7 +26,7 @@ function (angular, _, require, config) {
$scope.modalTitle = 'Share Dashboard';
}
if (!$scope.dashboardMeta.isSnapshot) {
if (!$scope.dashboard.meta.isSnapshot) {
$scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'});
}

View File

@ -34,7 +34,7 @@ export class SubmenuCtrl {
export function submenuDirective() {
return {
restrict: 'E',
templateUrl: 'app/features/dashboard/submenu/submenu.html',
templateUrl: 'public/app/features/dashboard/submenu/submenu.html',
controller: SubmenuCtrl,
bindToController: true,
controllerAs: 'ctrl',

View File

@ -1,47 +0,0 @@
<div class="gf-box-header">
<div class="gf-box-title">
<i class="fa fa-clock-o"></i>
Custom time range
</div>
<button class="gf-box-header-close-btn" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</button>
</div>
<div class="gf-box-body">
<div class="timepicker form-horizontal">
<form name="timeForm" style="margin-bottom: 0">
<div class="timepicker-from-column">
<label class="small">From</label>
<div class="fake-input timepicker-input">
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
</div>
</div>
<div class="timepicker-to-column">
<label class="small">To (<a class="link" ng-class="{'strong':temptime.now}" ng-click="ctrl.setNow();temptime.now=true">set now</a>)</label>
<div class="fake-input timepicker-input">
<div ng-hide="temptime.now">
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
</div>
<span type="text" ng-show="temptime.now" ng-disabled="temptime.now">&nbsp <i class="pointer fa fa-remove" ng-click="ctrl.setNow();temptime.now=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
</div>
</div>
<br>
<button ng-click="ctrl.setAbsoluteTimeFilter(ctrl.validate(temptime));dismiss();" ng-disabled="!timeForm.$valid" class="btn btn-success">Apply</button>
<span class="" ng-hide="input.$valid">Invalid date or range</span>
</form>
</div>
</div>

View File

@ -1,5 +1,5 @@
<div class="row pull-right">
<div class="gf-timepicker-absolute-section">
<form name="timeForm" class="gf-timepicker-absolute-section">
<h3>Time range</h3>
<label class="small">From:</label>
<div class="input-prepend">
@ -29,10 +29,10 @@
<select ng-model="ctrl.refresh.value" class='input-medium' ng-options="f.value as f.text for f in ctrl.refresh.options">
</select>
<button class="btn btn-inverse gf-timepicker-btn-apply" type="button" ng-click="ctrl.applyCustom()">
<button type="submit" class="btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">
Apply
</button>
</div>
</form>
<div class="gf-timepicker-relative-section">
<h3>Quick ranges</h3>

View File

@ -1,6 +1,7 @@
///<reference path="../../../headers/common.d.ts" />
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
export function inputDateDirective() {
return {
@ -11,8 +12,14 @@ export function inputDateDirective() {
var fromUser = function (text) {
if (text.indexOf('now') !== -1) {
if (!dateMath.isValid(text)) {
ngModel.$setValidity("error", false);
return undefined;
}
ngModel.$setValidity("error", true);
return text;
}
var parsed;
if ($scope.ctrl.isUtc) {
parsed = moment.utc(text, format);
@ -20,7 +27,13 @@ export function inputDateDirective() {
parsed = moment(text, format);
}
return parsed.isValid() ? parsed : undefined;
if (!parsed.isValid()) {
ngModel.$setValidity("error", false);
return undefined;
}
ngModel.$setValidity("error", true);
return parsed;
};
var toUser = function (currentValue) {

View File

@ -101,7 +101,7 @@ export class TimePickerCtrl {
this.refresh.options.unshift({text: 'off'});
this.$rootScope.appEvent('show-dash-editor', {
src: 'app/features/dashboard/timepicker/dropdown.html',
src: 'public/app/features/dashboard/timepicker/dropdown.html',
scope: this.$scope,
cssClass: 'gf-timepicker-dropdown',
});
@ -146,7 +146,7 @@ export class TimePickerCtrl {
export function settingsDirective() {
return {
restrict: 'E',
templateUrl: 'app/features/dashboard/timepicker/settings.html',
templateUrl: 'public/app/features/dashboard/timepicker/settings.html',
controller: TimePickerCtrl,
bindToController: true,
controllerAs: 'ctrl',
@ -159,7 +159,7 @@ export function settingsDirective() {
export function timePickerDirective() {
return {
restrict: 'E',
templateUrl: 'app/features/dashboard/timepicker/timepicker.html',
templateUrl: 'public/app/features/dashboard/timepicker/timepicker.html',
controller: TimePickerCtrl,
bindToController: true,
controllerAs: 'ctrl',

View File

@ -139,7 +139,7 @@ function(angular, _) {
};
$rootScope.appEvent('show-modal', {
src: './app/partials/unsaved-changes.html',
src: 'public/app/partials/unsaved-changes.html',
modalClass: 'modal-no-header confirm-modal',
scope: modalScope,
});

View File

@ -17,6 +17,7 @@ function (angular, _, $) {
self.state = {};
self.panelScopes = [];
self.$scope = $scope;
self.dashboard = $scope.dashboard;
$scope.exitFullscreen = function() {
if (self.state.fullscreen) {
@ -31,6 +32,14 @@ function (angular, _, $) {
}
});
$scope.onAppEvent('panel-change-view', function(evt, payload) {
self.update(payload);
});
$scope.onAppEvent('panel-instantiated', function(evt, payload) {
self.registerPanel(payload.scope);
});
this.update(this.getQueryStringState(), true);
this.expandRowForPanel();
}
@ -66,7 +75,7 @@ function (angular, _, $) {
DashboardViewState.prototype.update = function(state, skipUrlSync) {
_.extend(this.state, state);
this.fullscreen = this.state.fullscreen;
this.dashboard.meta.fullscreen = this.state.fullscreen;
if (!this.state.fullscreen) {
this.state.panelId = null;
@ -84,7 +93,7 @@ function (angular, _, $) {
DashboardViewState.prototype.syncState = function() {
if (this.panelScopes.length === 0) { return; }
if (this.fullscreen) {
if (this.dashboard.meta.fullscreen) {
if (this.fullscreenPanel) {
this.leaveFullscreen(false);
}
@ -105,23 +114,24 @@ function (angular, _, $) {
DashboardViewState.prototype.getPanelScope = function(id) {
return _.find(this.panelScopes, function(panelScope) {
return panelScope.panel.id === id;
return panelScope.ctrl.panel.id === id;
});
};
DashboardViewState.prototype.leaveFullscreen = function(render) {
var self = this;
var ctrl = self.fullscreenPanel.ctrl;
self.fullscreenPanel.editMode = false;
self.fullscreenPanel.fullscreen = false;
delete self.fullscreenPanel.height;
ctrl.editMode = false;
ctrl.fullscreen = false;
delete ctrl.height;
this.$scope.appEvent('panel-fullscreen-exit', {panelId: this.fullscreenPanel.panel.id});
this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id});
if (!render) { return false;}
$timeout(function() {
if (self.oldTimeRange !== self.fullscreenPanel.range) {
if (self.oldTimeRange !== ctrl.range) {
self.$scope.broadcastRefresh();
}
else {
@ -135,17 +145,18 @@ function (angular, _, $) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
var ctrl = panelScope.ctrl;
panelScope.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
panelScope.height = panelScope.editMode ? editHeight : fullscreenHeight;
ctrl.editMode = this.state.edit && this.$scope.dashboardMeta.canEdit;
ctrl.height = ctrl.editMode ? editHeight : fullscreenHeight;
ctrl.fullscreen = true;
this.oldTimeRange = panelScope.range;
this.oldTimeRange = ctrl.range;
this.fullscreenPanel = panelScope;
$(window).scrollTop(0);
panelScope.fullscreen = true;
this.$scope.appEvent('panel-fullscreen-enter', {panelId: panelScope.panel.id});
this.$scope.appEvent('panel-fullscreen-enter', {panelId: ctrl.panel.id});
$timeout(function() {
panelScope.$broadcast('render');
@ -156,8 +167,12 @@ function (angular, _, $) {
var self = this;
self.panelScopes.push(panelScope);
if (self.state.panelId === panelScope.panel.id) {
self.enterFullscreen(panelScope);
if (self.state.panelId === panelScope.ctrl.panel.id) {
if (self.state.edit) {
panelScope.ctrl.editPanel();
} else {
panelScope.ctrl.viewPanel();
}
}
panelScope.$on('$destroy', function() {

View File

@ -21,7 +21,7 @@ function (angular, _) {
return {
restrict: 'E',
controller: 'DashLinkEditorCtrl',
templateUrl: 'app/features/dashlinks/editor.html',
templateUrl: 'public/app/features/dashlinks/editor.html',
link: function() {
}
};

View File

@ -10,7 +10,7 @@ function (angular, _, config) {
var datasourceTypes = [];
module.directive('datasourceHttpSettings', function() {
return {templateUrl: 'app/features/datasources/partials/http_settings.html'};
return {templateUrl: 'public/app/features/datasources/partials/http_settings.html'};
});
module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {

View File

@ -1,9 +1,9 @@
<topnav title="Data sources" title-url="datasources" icon="fa fa-fw fa-database" subnav="true">
<navbar title="Data sources" title-url="datasources" icon="fa fa-fw fa-database" subnav="true">
<ul class="nav">
<li ng-class="{active: isNew}" ng-show="isNew"><a href="datasources/new">Add new</a></li>
<li class="active" ng-show="!isNew"><a href="datasources/edit/{{current.name}}">{{current.name}}</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">

View File

@ -1,10 +1,10 @@
<topnav title="Data sources" icon="fa fa-fw fa-database" subnav="false">
</topnav>
<navbar title="Data sources" icon="fa fa-fw fa-database">
</navbar>
<div class="page-container">
<div class="page-wide">
<a type="submit" class="btn btn-inverse pull-right" href="datasources/new">
<a class="btn btn-inverse pull-right" href="datasources/new">
<i class="fa fa-plus"></i>
Add data source
</a>

View File

@ -32,7 +32,7 @@ function (angular) {
modalScope.key = result.key;
$scope.appEvent('show-modal', {
src: './app/features/org/partials/apikeyModal.html',
src: 'public/app/features/org/partials/apikeyModal.html',
scope: modalScope
});

View File

@ -54,7 +54,7 @@ function (angular) {
};
$scope.appEvent('show-modal', {
src: './app/features/org/partials/invite.html',
src: 'public/app/features/org/partials/invite.html',
modalClass: 'modal-no-header invite-modal',
scope: modalScope
});

View File

@ -1,8 +1,8 @@
<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
<navbar title="Organization" icon="fa fa-fw fa-users" subnav="true">
<ul class="nav">
<li class="active"><a href="org/new">New organization</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">

View File

@ -1,8 +1,8 @@
<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
<navbar icon="fa fa-fw fa-users" title="Organization" subnav="true">
<ul class="nav">
<li class="active"><a href="org/apikeys">API Keys</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page-wide">

View File

@ -1,8 +1,8 @@
<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
<navbar icon="fa fa-fw fa-users" title="Organization">
<ul class="nav">
<li class="active"><a href="org">Preferences</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page">

View File

@ -1,8 +1,8 @@
<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
<navbar title="Organization" icon="fa fa-fw fa-users" subnav="true">
<ul class="nav">
<li class="active"><a href="org/users">Users</a></li>
</ul>
</topnav>
</navbar>
<div class="page-container">
<div class="page-wide">

View File

@ -1,9 +1,8 @@
define([
'./panel_menu',
'./panel_directive',
'./panel_srv',
'./panel_helper',
'./solo_panel_ctrl',
'./panel_loader',
'./query_editor',
'./panel_editor_tab',
], function () {});

View File

@ -0,0 +1,234 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import $ from 'jquery';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import {PanelCtrl} from './panel_ctrl';
import * as rangeUtil from '../../core/utils/rangeutil';
import * as dateMath from '../../core/utils/datemath';
class MetricsPanelCtrl extends PanelCtrl {
error: boolean;
loading: boolean;
datasource: any;
$q: any;
$timeout: any;
datasourceSrv: any;
timeSrv: any;
timing: any;
range: any;
rangeRaw: any;
interval: any;
resolution: any;
timeInfo: any;
skipDataOnInit: boolean;
datasources: any[];
constructor($scope, $injector) {
super($scope, $injector);
// make metrics tab the default
this.editorTabIndex = 1;
this.$q = $injector.get('$q');
this.datasourceSrv = $injector.get('datasourceSrv');
this.timeSrv = $injector.get('timeSrv');
if (!this.panel.targets) {
this.panel.targets = [{}];
}
// hookup initial data fetch
this.$timeout(() => {
if (!this.skipDataOnInit) {
this.refresh();
}
}, 30);;
}
initEditMode() {
this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
this.datasources = this.datasourceSrv.getMetricSources();
}
refresh() {
this.getData();
}
refreshData(data) {
// null op
return this.$q.when(data);
}
loadSnapshot(data) {
// null op
return data;
}
getData() {
// ignore fetching data if another panel is in fullscreen
if (this.otherPanelInFullscreenMode()) { return; }
// if we have snapshot data use that
if (this.panel.snapshotData) {
if (this.loadSnapshot) {
this.loadSnapshot(this.panel.snapshotData);
}
return;
}
// clear loading/error state
delete this.error;
this.loading = true;
// load datasource service
this.datasourceSrv.get(this.panel.datasource).then(datasource => {
this.datasource = datasource;
return this.refreshData(this.datasource);
}).then(() => {
this.loading = false;
}).catch(err => {
console.log('Panel data error:', err);
this.loading = false;
this.error = err.message || "Timeseries data request error";
this.inspector = {error: err};
});
}
setTimeQueryStart() {
this.timing = {};
this.timing.queryStart = new Date().getTime();
}
setTimeQueryEnd() {
this.timing.queryEnd = new Date().getTime();
}
updateTimeRange() {
this.range = this.timeSrv.timeRange();
this.rangeRaw = this.timeSrv.timeRange(false);
this.applyPanelTimeOverrides();
if (this.panel.maxDataPoints) {
this.resolution = this.panel.maxDataPoints;
} else {
this.resolution = Math.ceil($(window).width() * (this.panel.span / 12));
}
var panelInterval = this.panel.interval;
var datasourceInterval = (this.datasource || {}).interval;
this.interval = kbn.calculateInterval(this.range, this.resolution, panelInterval || datasourceInterval);
};
applyPanelTimeOverrides() {
this.timeInfo = '';
// check panel time overrrides
if (this.panel.timeFrom) {
var timeFromInfo = rangeUtil.describeTextRange(this.panel.timeFrom);
if (timeFromInfo.invalid) {
this.timeInfo = 'invalid time override';
return;
}
if (_.isString(this.rangeRaw.from)) {
var timeFromDate = dateMath.parse(timeFromInfo.from);
this.timeInfo = timeFromInfo.display;
this.rangeRaw.from = timeFromInfo.from;
this.rangeRaw.to = timeFromInfo.to;
this.range.from = timeFromDate;
}
}
if (this.panel.timeShift) {
var timeShiftInfo = rangeUtil.describeTextRange(this.panel.timeShift);
if (timeShiftInfo.invalid) {
this.timeInfo = 'invalid timeshift';
return;
}
var timeShift = '-' + this.panel.timeShift;
this.timeInfo += ' timeshift ' + timeShift;
this.range.from = dateMath.parseDateMath(timeShift, this.range.from, false);
this.range.to = dateMath.parseDateMath(timeShift, this.range.to, true);
this.rangeRaw = this.range;
}
if (this.panel.hideTimeOverride) {
this.timeInfo = '';
}
};
issueQueries(datasource) {
if (!this.panel.targets || this.panel.targets.length === 0) {
return this.$q.when([]);
}
this.updateTimeRange();
var metricsQuery = {
range: this.range,
rangeRaw: this.rangeRaw,
interval: this.interval,
targets: this.panel.targets,
format: this.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: this.resolution,
scopedVars: this.panel.scopedVars,
cacheTimeout: this.panel.cacheTimeout
};
this.setTimeQueryStart();
return datasource.query(metricsQuery).then(results => {
this.setTimeQueryEnd();
if (this.dashboard.snapshot) {
this.panel.snapshotData = results;
}
return results;
});
}
addDataQuery(datasource) {
this.dashboard.addDataQueryTo(this.panel, datasource);
}
removeDataQuery(query) {
this.dashboard.removeDataQuery(this.panel, query);
this.refresh();
};
duplicateDataQuery(query) {
this.dashboard.duplicateDataQuery(this.panel, query);
}
moveDataQuery(fromIndex, toIndex) {
this.dashboard.moveDataQuery(this.panel, fromIndex, toIndex);
}
setDatasource(datasource) {
// switching to mixed
if (datasource.meta.mixed) {
_.each(this.panel.targets, target => {
target.datasource = this.panel.datasource;
if (target.datasource === null) {
target.datasource = config.defaultDatasource;
}
});
} else if (this.datasource && this.datasource.meta.mixed) {
_.each(this.panel.targets, target => {
delete target.datasource;
});
}
this.panel.datasource = datasource.value;
this.datasource = null;
this.refresh();
}
}
export {MetricsPanelCtrl};

View File

@ -0,0 +1,51 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import {PanelCtrl} from './panel_ctrl';
import {MetricsPanelCtrl} from './metrics_panel_ctrl';
export class DefaultPanelCtrl extends PanelCtrl {
/** @ngInject */
constructor($scope, $injector) {
super($scope, $injector);
}
}
class PanelDirective {
template: string;
templateUrl: string;
bindToController: boolean;
scope: any;
controller: any;
controllerAs: string;
getDirective() {
if (!this.controller) {
this.controller = DefaultPanelCtrl;
}
return {
template: this.template,
templateUrl: this.templateUrl,
controller: this.controller,
controllerAs: 'ctrl',
bindToController: true,
scope: {dashboard: "=", panel: "=", row: "="},
link: (scope, elem, attrs, ctrl) => {
ctrl.init();
this.link(scope, elem, attrs, ctrl);
}
};
}
link(scope, elem, attrs, ctrl) {
return null;
}
}
export {
PanelCtrl,
MetricsPanelCtrl,
PanelDirective,
}

View File

@ -0,0 +1,179 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import _ from 'lodash';
export class PanelCtrl {
panel: any;
row: any;
dashboard: any;
editorTabIndex: number;
pluginName: string;
pluginId: string;
icon: string;
editorTabs: any;
$scope: any;
$injector: any;
$timeout: any;
fullscreen: boolean;
inspector: any;
editModeInitiated: boolean;
editorHelpIndex: number;
constructor($scope, $injector) {
this.$injector = $injector;
this.$scope = $scope;
this.$timeout = $injector.get('$timeout');
this.editorTabIndex = 0;
var plugin = config.panels[this.panel.type];
if (plugin) {
this.pluginId = plugin.id;
this.pluginName = plugin.name;
}
$scope.$on("refresh", () => this.refresh());
}
init() {
this.publishAppEvent('panel-instantiated', {scope: this.$scope});
this.refresh();
}
renderingCompleted() {
this.$scope.$root.performance.panelsRendered++;
}
refresh() {
return;
}
publishAppEvent(evtName, evt) {
this.$scope.$root.appEvent(evtName, evt);
}
changeView(fullscreen, edit) {
this.publishAppEvent('panel-change-view', {
fullscreen: fullscreen, edit: edit, panelId: this.panel.id
});
}
viewPanel() {
this.changeView(true, false);
}
editPanel() {
if (!this.editModeInitiated) {
this.editorTabs = [];
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
this.initEditMode();
}
this.changeView(true, true);
}
exitFullscreen() {
this.changeView(false, false);
}
initEditMode() {
return;
}
addEditorTab(title, directiveFn, index?) {
var editorTab = {title, directiveFn};
if (_.isString(directiveFn)) {
editorTab.directiveFn = function() {
return {templateUrl: directiveFn};
};
}
if (index) {
this.editorTabs.splice(index, 0, editorTab);
} else {
this.editorTabs.push(editorTab);
}
}
getMenu() {
let menu = [];
menu.push({text: 'View', click: 'ctrl.viewPanel(); dismiss();'});
menu.push({text: 'Edit', click: 'ctrl.editPanel(); dismiss();', role: 'Editor'});
menu.push({text: 'Duplicate', click: 'ctrl.duplicate()', role: 'Editor' });
menu.push({text: 'Share', click: 'ctrl.sharePanel(); dismiss();'});
return menu;
}
getExtendedMenu() {
return [{text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();'}];
}
otherPanelInFullscreenMode() {
return this.dashboard.meta.fullscreen && !this.fullscreen;
}
broadcastRender(arg1?, arg2?) {
this.$scope.$broadcast('render', arg1, arg2);
}
toggleEditorHelp(index) {
if (this.editorHelpIndex === index) {
this.editorHelpIndex = null;
return;
}
this.editorHelpIndex = index;
}
duplicate() {
this.dashboard.duplicatePanel(this.panel, this.row);
}
updateColumnSpan(span) {
this.panel.span = Math.min(Math.max(Math.floor(this.panel.span + span), 1), 12);
this.$timeout(() => {
this.broadcastRender();
});
}
removePanel() {
this.publishAppEvent('confirm-modal', {
title: 'Are you sure you want to remove this panel?',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
this.row.panels = _.without(this.row.panels, this.panel);
}
});
}
editPanelJson() {
this.publishAppEvent('show-json-editor', {
object: this.panel,
updateHandler: this.replacePanel.bind(this)
});
}
replacePanel(newPanel, oldPanel) {
var row = this.row;
var index = _.indexOf(this.row.panels, oldPanel);
this.row.panels.splice(index, 1);
// adding it back needs to be done in next digest
this.$timeout(() => {
newPanel.id = oldPanel.id;
newPanel.span = oldPanel.span;
this.row.panels.splice(index, 0, newPanel);
});
}
sharePanel() {
var shareScope = this.$scope.$new();
shareScope.panel = this.panel;
shareScope.dashboard = this.dashboard;
this.publishAppEvent('show-modal', {
src: 'public/app/features/dashboard/partials/shareModal.html',
scope: shareScope
});
}
}

View File

@ -10,14 +10,15 @@ function (angular, $) {
module.directive('grafanaPanel', function() {
return {
restrict: 'E',
templateUrl: 'app/features/panel/partials/panel.html',
templateUrl: 'public/app/features/panel/partials/panel.html',
transclude: true,
scope: { ctrl: "=" },
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() {
panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' });
elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
var ctrl = scope.ctrl;
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
});
}
};
@ -30,6 +31,7 @@ function (angular, $) {
link: function(scope, elem) {
var resizing = false;
var lastPanel = false;
var ctrl = scope.ctrl;
var handleOffset;
var originalHeight;
var originalWidth;
@ -40,31 +42,31 @@ function (angular, $) {
resizing = true;
handleOffset = $(e.target).offset();
originalHeight = parseInt(scope.row.height);
originalWidth = scope.panel.span;
originalHeight = parseInt(ctrl.row.height);
originalWidth = ctrl.panel.span;
maxWidth = $(document).width();
lastPanel = scope.row.panels[scope.row.panels.length - 1];
lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
$('body').on('mousemove', moveHandler);
$('body').on('mouseup', dragEndHandler);
}
function moveHandler(e) {
scope.row.height = originalHeight + (e.pageY - handleOffset.top);
scope.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
scope.panel.span = Math.min(Math.max(scope.panel.span, 1), 12);
ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
var rowSpan = scope.dashboard.rowSpan(scope.row);
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
// auto adjust other panels
if (Math.floor(rowSpan) < 14) {
// last panel should not push row down
if (lastPanel === scope.panel && rowSpan > 12) {
if (lastPanel === ctrl.panel && rowSpan > 12) {
lastPanel.span -= rowSpan - 12;
}
// reduce width of last panel so total in row is 12
else if (lastPanel !== scope.panel) {
else if (lastPanel !== ctrl.panel) {
lastPanel.span = lastPanel.span - (rowSpan - 12);
lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
}
@ -77,7 +79,7 @@ function (angular, $) {
function dragEndHandler() {
// if close to 12
var rowSpan = scope.dashboard.rowSpan(scope.row);
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
if (rowSpan < 12 && rowSpan > 11) {
lastPanel.span += 12 - rowSpan;
}

View File

@ -0,0 +1,28 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
var directiveModule = angular.module('grafana.directives');
/** @ngInject */
function panelEditorTab(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
ctrl: "=",
editorTab: "=",
index: "=",
},
directive: scope => {
var pluginId = scope.ctrl.pluginId;
var tabIndex = scope.index;
return Promise.resolve({
name: `panel-editor-tab-${pluginId}${tabIndex}`,
fn: scope.editorTab.directiveFn,
});
}
});
}
directiveModule.directive('panelEditorTab', panelEditorTab);

View File

@ -3,28 +3,86 @@
import angular from 'angular';
import config from 'app/core/config';
import {unknownPanelDirective} from '../../plugins/panel/unknown/module';
import {UnknownPanel} from '../../plugins/panel/unknown/module';
var directiveModule = angular.module('grafana.directives');
/** @ngInject */
function panelLoader($parse, dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
directive: scope => {
let panelInfo = config.panels[scope.panel.type];
if (!panelInfo) {
return Promise.resolve({
name: 'panel-directive-' + scope.panel.type,
fn: unknownPanelDirective
function panelLoader($compile, dynamicDirectiveSrv, $http, $q, $injector, $templateCache) {
return {
restrict: 'E',
scope: {
dashboard: "=",
row: "=",
panel: "="
},
link: function(scope, elem, attrs) {
function getTemplate(directive) {
if (directive.template) {
return $q.when(directive.template);
}
var cached = $templateCache.get(directive.templateUrl);
if (cached) {
return $q.when(cached);
}
return $http.get(directive.templateUrl).then(res => {
return res.data;
});
}
return System.import(panelInfo.module).then(function(panelModule) {
return {
name: 'panel-directive-' + scope.panel.type,
fn: panelModule.panel,
};
function addPanelAndCompile(name) {
var child = angular.element(document.createElement(name));
child.attr('dashboard', 'dashboard');
child.attr('panel', 'panel');
child.attr('row', 'row');
$compile(child)(scope);
elem.empty();
elem.append(child);
}
function addPanel(name, Panel) {
if (Panel.registered) {
addPanelAndCompile(name);
return;
}
if (Panel.promise) {
Panel.promise.then(() => {
addPanelAndCompile(name);
});
return;
}
var panelInstance = $injector.instantiate(Panel);
var directive = panelInstance.getDirective();
Panel.promise = getTemplate(directive).then(template => {
directive.templateUrl = null;
directive.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
directiveModule.directive(attrs.$normalize(name), function() {
return directive;
});
Panel.registered = true;
addPanelAndCompile(name);
});
}
var panelElemName = 'panel-directive-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type];
if (!panelInfo) {
addPanel(panelElemName, UnknownPanel);
return;
}
System.import(panelInfo.module).then(function(panelModule) {
addPanel(panelElemName, panelModule.Panel);
}).catch(err => {
console.log('Panel err: ', err);
});
},
});
}
};
}
angular.module('grafana.directives').directive('panelLoader', panelLoader);
directiveModule.directive('panelLoader', panelLoader);

View File

@ -11,32 +11,33 @@ function (angular, $, _) {
.directive('panelMenu', function($compile, linkSrv) {
var linkTemplate =
'<span class="panel-title drag-handle pointer">' +
'<span class="panel-title-text drag-handle">{{panel.title | interpolateTemplateVars:this}}</span>' +
'<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
'<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
'<span class="panel-time-info" ng-show="panelMeta.timeInfo"><i class="fa fa-clock-o"></i> {{panelMeta.timeInfo}}</span>' +
'<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +
'</span>';
function createExternalLinkMenu($scope) {
function createExternalLinkMenu(ctrl) {
var template = '<div class="panel-menu small">';
template += '<div class="panel-menu-row">';
if ($scope.panel.links) {
_.each($scope.panel.links, function(link) {
var info = linkSrv.getPanelLinkAnchorInfo(link, $scope.panel.scopedVars);
if (ctrl.panel.links) {
_.each(ctrl.panel.links, function(link) {
var info = linkSrv.getPanelLinkAnchorInfo(link, ctrl.panel.scopedVars);
template += '<a class="panel-menu-link" href="' + info.href + '" target="' + info.target + '">' + info.title + '</a>';
});
}
return template;
}
function createMenuTemplate($scope) {
function createMenuTemplate(ctrl) {
var template = '<div class="panel-menu small">';
if ($scope.dashboardMeta.canEdit) {
if (ctrl.dashboard.meta.canEdit) {
template += '<div class="panel-menu-inner">';
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="removePanel(panel)"><i class="fa fa-remove"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-remove"></i></a>';
template += '<div class="clearfix"></div>';
template += '</div>';
}
@ -44,9 +45,9 @@ function (angular, $, _) {
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="fa fa-bars"></i></a>';
_.each($scope.panelMeta.menu, function(item) {
_.each(ctrl.getMenu(), function(item) {
// skip edit actions if not editor
if (item.role === 'Editor' && !$scope.dashboardMeta.canEdit) {
if (item.role === 'Editor' && !ctrl.dashboard.meta.canEdit) {
return;
}
@ -63,8 +64,8 @@ function (angular, $, _) {
return template;
}
function getExtendedMenu($scope) {
return angular.copy($scope.panelMeta.extendedMenu);
function getExtendedMenu(ctrl) {
return ctrl.getExtendedMenu();
}
return {
@ -74,13 +75,14 @@ function (angular, $, _) {
var $panelLinksBtn = $link.find(".panel-links-btn");
var $panelContainer = elem.parents(".panel-container");
var menuScope = null;
var ctrl = $scope.ctrl;
var timeout = null;
var $menu = null;
elem.append($link);
$scope.$watchCollection('panel.links', function(newValue) {
var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== '';
$scope.$watchCollection('ctrl.panel.links', function(newValue) {
var showIcon = (newValue ? newValue.length > 0 : false) && ctrl.panel.title !== '';
$panelLinksBtn.toggle(showIcon);
});
@ -95,7 +97,7 @@ function (angular, $, _) {
// if hovering or draging pospone close
if (force !== true) {
if ($menu.is(':hover') || $scope.dashboard.$$panelDragging) {
if ($menu.is(':hover') || $scope.ctrl.dashboard.$$panelDragging) {
dismiss(2200);
return;
}
@ -124,9 +126,9 @@ function (angular, $, _) {
var menuTemplate;
if ($(e.target).hasClass('fa-external-link')) {
menuTemplate = createExternalLinkMenu($scope);
menuTemplate = createExternalLinkMenu(ctrl);
} else {
menuTemplate = createMenuTemplate($scope);
menuTemplate = createMenuTemplate(ctrl);
}
$menu = $(menuTemplate);
@ -135,7 +137,7 @@ function (angular, $, _) {
});
menuScope = $scope.$new();
menuScope.extendedMenu = getExtendedMenu($scope);
menuScope.extendedMenu = getExtendedMenu(ctrl);
menuScope.dismiss = function() {
dismiss(null, true);
};

View File

@ -1,7 +0,0 @@
define([
'./panel_meta2',
],
function (panelMeta) {
'use strict';
return panelMeta.default;
});

View File

@ -1,48 +0,0 @@
export default class PanelMeta {
description: any;
fullscreen: any;
editIcon: any;
panelName: any;
menu: any;
editorTabs: any;
extendedMenu: any;
constructor(options: any) {
this.description = options.description;
this.fullscreen = options.fullscreen;
this.editIcon = options.editIcon;
this.panelName = options.panelName;
this.menu = [];
this.editorTabs = [];
this.extendedMenu = [];
if (options.fullscreen) {
this.addMenuItem('View', 'icon-eye-open', 'toggleFullscreen(false); dismiss();');
}
this.addMenuItem('Edit', 'icon-cog', 'editPanel(); dismiss();', 'Editor');
this.addMenuItem('Duplicate', 'icon-copy', 'duplicatePanel()', 'Editor');
this.addMenuItem('Share', 'icon-share', 'sharePanel(); dismiss();');
this.addEditorTab('General', 'app/partials/panelgeneral.html');
if (options.metricsEditor) {
this.addEditorTab('Metrics', 'app/partials/metrics.html');
}
this.addExtendedMenuItem('Panel JSON', '', 'editPanelJson(); dismiss();');
}
addMenuItem (text, icon, click, role?) {
this.menu.push({text: text, icon: icon, click: click, role: role});
}
addExtendedMenuItem (text, icon, click, role?) {
this.extendedMenu.push({text: text, icon: icon, click: click, role: role});
}
addEditorTab (title, src) {
this.editorTabs.push({title: title, src: src});
}
}

View File

@ -1,162 +0,0 @@
define([
'angular',
'lodash',
'app/core/config',
],
function (angular, _, config) {
'use strict';
var module = angular.module('grafana.services');
module.service('panelSrv', function($rootScope, $timeout, datasourceSrv, $q) {
this.init = function($scope) {
if (!$scope.panel.span) { $scope.panel.span = 12; }
$scope.inspector = {};
$scope.editPanel = function() {
$scope.toggleFullscreen(true);
};
$scope.sharePanel = function() {
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/shareModal.html',
scope: $scope.$new()
});
};
$scope.editPanelJson = function() {
$scope.appEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
};
$scope.duplicatePanel = function() {
$scope.dashboard.duplicatePanel($scope.panel, $scope.row);
};
$scope.updateColumnSpan = function(span) {
$scope.updatePanelSpan($scope.panel, span);
$timeout(function() {
$scope.$broadcast('render');
});
};
$scope.addDataQuery = function(datasource) {
$scope.dashboard.addDataQueryTo($scope.panel, datasource);
};
$scope.removeDataQuery = function (query) {
$scope.dashboard.removeDataQuery($scope.panel, query);
$scope.get_data();
};
$scope.duplicateDataQuery = function(query) {
$scope.dashboard.duplicateDataQuery($scope.panel, query);
};
$scope.moveDataQuery = function(fromIndex, toIndex) {
$scope.dashboard.moveDataQuery($scope.panel, fromIndex, toIndex);
};
$scope.setDatasource = function(datasource) {
// switching to mixed
if (datasource.meta.mixed) {
_.each($scope.panel.targets, function(target) {
target.datasource = $scope.panel.datasource;
if (target.datasource === null) {
target.datasource = config.defaultDatasource;
}
});
}
// switching from mixed
else if ($scope.datasource && $scope.datasource.meta.mixed) {
_.each($scope.panel.targets, function(target) {
delete target.datasource;
});
}
$scope.panel.datasource = datasource.value;
$scope.datasource = null;
$scope.get_data();
};
$scope.toggleEditorHelp = function(index) {
if ($scope.editorHelpIndex === index) {
$scope.editorHelpIndex = null;
return;
}
$scope.editorHelpIndex = index;
};
$scope.isNewPanel = function() {
return $scope.panel.title === config.new_panel_title;
};
$scope.toggleFullscreen = function(edit) {
$scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
};
$scope.otherPanelInFullscreenMode = function() {
return $scope.dashboardViewState.fullscreen && !$scope.fullscreen;
};
$scope.getCurrentDatasource = function() {
if ($scope.datasource) {
return $q.when($scope.datasource);
}
return datasourceSrv.get($scope.panel.datasource);
};
$scope.panelRenderingComplete = function() {
$rootScope.performance.panelsRendered++;
};
$scope.get_data = function() {
if ($scope.otherPanelInFullscreenMode()) { return; }
if ($scope.panel.snapshotData) {
if ($scope.loadSnapshot) {
$scope.loadSnapshot($scope.panel.snapshotData);
}
return;
}
delete $scope.panelMeta.error;
$scope.panelMeta.loading = true;
$scope.getCurrentDatasource().then(function(datasource) {
$scope.datasource = datasource;
return $scope.refreshData($scope.datasource) || $q.when({});
}).then(function() {
$scope.panelMeta.loading = false;
}, function(err) {
console.log('Panel data error:', err);
$scope.panelMeta.loading = false;
$scope.panelMeta.error = err.message || "Timeseries data request error";
$scope.inspector.error = err;
});
};
if ($scope.refreshData) {
$scope.$on("refresh", $scope.get_data);
}
// Post init phase
$scope.fullscreen = false;
$scope.editor = { index: 1 };
$scope.dashboardViewState.registerPanel($scope);
$scope.datasources = datasourceSrv.getMetricSources();
if (!$scope.skipDataOnInit) {
$timeout(function() {
$scope.get_data();
}, 30);
}
};
});
});

View File

@ -1,12 +1,12 @@
<div class="panel-container" ng-class="{'panel-transparent': panel.transparent}">
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
<div class="panel-header">
<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="panelMeta.error">
<span data-placement="top" bs-tooltip="panelMeta.error">
<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="ctrl.error">
<span data-placement="top" bs-tooltip="ctrl.error">
<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
</span>
</span>
<span class="panel-loading" ng-show="panelMeta.loading">
<span class="panel-loading" ng-show="ctrl.loading">
<i class="fa fa-spinner fa-spin"></i>
</span>
@ -19,27 +19,27 @@
<panel-resizer></panel-resizer>
</div>
<div class="panel-full-edit" ng-if="editMode">
<div class="panel-full-edit" ng-if="ctrl.editMode">
<div class="gf-box">
<div class="gf-box-header">
<div class="gf-box-title">
<i ng-class="panelMeta.editIcon"></i>
{{panelMeta.panelName}}
<i ng-class="ctrl.icon"></i>
{{ctrl.name}}
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
<div ng-model="ctrl.editorTabIndex" bs-tabs>
<div ng-repeat="tab in ctrl.editorTabs" data-title="{{tab.title}}">
</div>
</div>
<button class="gf-box-header-close-btn" ng-click="exitFullscreen();">
<button class="gf-box-header-close-btn" ng-click="ctrl.exitFullscreen();">
Back to dashboard
</button>
</div>
<div class="gf-box-body">
<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
<div ng-include src="tab.src"></div>
<div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index">
<panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
</div>
</div>
</div>

View File

@ -13,8 +13,8 @@
</li>
<li>
<input type="text" class="input-small tight-form-input last" placeholder="1h"
empty-to-null ng-model="panel.timeFrom" valid-time-span
ng-change="get_data()" ng-model-onblur>
empty-to-null ng-model="ctrl.panel.timeFrom" valid-time-span
ng-change="ctrl.refresh()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
@ -32,8 +32,8 @@
</li>
<li>
<input type="text" class="input-small tight-form-input last" placeholder="1h"
empty-to-null ng-model="panel.timeShift" valid-time-span
ng-change="get_data()" ng-model-onblur>
empty-to-null ng-model="ctrl.panel.timeShift" valid-time-span
ng-change="ctrl.refresh()" ng-model-onblur>
</li>
</ul>
<div class="clearfix"></div>
@ -47,9 +47,9 @@
<strong>Hide time override info</strong>
</li>
<li class="tight-form-item last">
<input class="cr1" id="panel.hideTimeOverride" type="checkbox"
ng-model="panel.hideTimeOverride" ng-checked="panel.hideTimeOverride" ng-change="get_data()">
<label for="panel.hideTimeOverride" class="cr1"></label>
<input class="cr1" id="ctrl.panel.hideTimeOverride" type="checkbox"
ng-model="ctrl.panel.hideTimeOverride" ng-checked="ctrl.panel.hideTimeOverride" ng-change="ctrl.refresh()">
<label for="ctrl.panel.hideTimeOverride" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -2,7 +2,8 @@
<div class="row-fluid">
<div class="span12">
<div class="panel nospace" ng-if="panel" style="width: 100%">
<panel-loader type="panel.type" ng-cloak></panel-loader>
<panel-loader dashboard="dashboard" row="row" panel="panel">
</panel-loader>
</div>
</div>
</div>

View File

@ -5,9 +5,9 @@ import angular from 'angular';
/** @ngInject */
function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watchPath: "panel.datasource",
watchPath: "ctrl.panel.datasource",
directive: scope => {
let datasource = scope.target.datasource || scope.panel.datasource;
let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
return datasourceSrv.get(datasource).then(ds => {
scope.datasource = ds;
@ -29,9 +29,9 @@ function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
/** @ngInject */
function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watchPath: "panel.datasource",
watchPath: "ctrl.panel.datasource",
directive: scope => {
return datasourceSrv.get(scope.panel.datasource).then(ds => {
return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'metrics-query-options-' + ds.meta.id,

View File

@ -39,13 +39,8 @@ function (angular, $) {
}
$scope.panel.span = 12;
$scope.dashboardViewState = {registerPanel: function() { }, state: {}};
};
if (!$scope.skipAutoInit) {
$scope.init();
}
$scope.init();
});
});

View File

@ -15,7 +15,7 @@ function (angular, _) {
},
restrict: 'E',
controller: 'PanelLinksEditorCtrl',
templateUrl: 'app/features/panellinks/module.html',
templateUrl: 'public/app/features/panellinks/module.html',
link: function() {
}
};

View File

@ -1,5 +1,6 @@
define([
'./playlists_ctrl',
'./playlist_search',
'./playlist_srv',
'./playlist_edit_ctrl',
'./playlist_routes'

View File

@ -1,14 +1,14 @@
<navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true">
<ul class="nav">
<li ng-class="{active: isNew()}" ng-show="isNew()"><a href="datasources/create">New</a></li>
<li class="active" ng-show="!isNew()"><a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a></li>
<li ng-class="{active: ctrl.isNew()}" ng-show="ctrl.isNew()"><a href="datasources/create">New</a></li>
<li class="active" ng-show="!ctrl.isNew()"><a href="playlists/edit/{{ctrl.playlist.id}}">{{ctrl.playlist.name}}</a></li>
</ul>
</navbar>
<div class="page-container" ng-form="playlistEditForm">
<div class="page">
<h2 ng-show="isNew()">New playlist</h2>
<h2 ng-show="!isNew()">Edit playlist</h2>
<h2 ng-show="ctrl.isNew()">New playlist</h2>
<h2 ng-show="!ctrl.isNew()">Edit playlist</h2>
<h4>Name and interval</h4>
@ -20,7 +20,7 @@
Name
</li>
<li>
<input type="text" required ng-model="playlist.name" class="input-xlarge tight-form-input">
<input type="text" required ng-model="ctrl.playlist.name" class="input-xlarge tight-form-input">
</li>
</ul>
<div class="clearfix"></div>
@ -31,7 +31,7 @@
Interval
</li>
<li>
<input type="text" required ng-model="playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
<input type="text" required ng-model="ctrl.playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
</li>
</ul>
<div class="clearfix"></div>
@ -39,66 +39,72 @@
</div>
<br>
<h4>Add dashboards</h4>
<div style="display: inline-block">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">
Search
</li>
<li>
<input type="text"
class="tight-form-input input-xlarge last"
ng-model="searchQuery"
placeholder="dashboard search term"
ng-trim="true"
ng-change="search()">
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="span5 pull-left">
<h5>Search results ({{filteredPlaylistItems.length}})</h5>
<h5>Add dashboards</h5>
<div style="">
<playlist-search class="playlist-search-container" search-started="ctrl.searchStarted(promise)"></playlist-search>
</div>
</div>
</div>
<div class="row">
<div class="span5 pull-left" ng-if="ctrl.filteredDashboards.length > 0">
<h5>Search results ({{ctrl.filteredDashboards.length}})</h5>
<table class="grafana-options-table">
<tr ng-repeat="playlistItem in filteredPlaylistItems">
<tr ng-repeat="playlistItem in ctrl.filteredDashboards">
<td style="white-space: nowrap;">
{{playlistItem.title}}
</td>
<td style="text-align: center">
<button class="btn btn-inverse btn-mini pull-right" ng-click="addPlaylistItem(playlistItem)">
<button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addPlaylistItem(playlistItem)">
<i class="fa fa-plus"></i>
Add to playlist
</button>
</td>
</tr>
<tr ng-if="isSearchResultsEmpty()">
<td colspan="2">
<i class="fa fa-warning"></i> Search results empty
</td>
</tr>
</table>
</div>
<div class="playlist-search-results-container" ng-if="ctrl.filteredTags.length > 0">
<div class="row">
<div class="span6 offset1">
<div ng-repeat="tag in ctrl.filteredTags" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === selectedIndex }"
ng-click="ctrl.addTagPlaylistItem(tag, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
<i class="fa fa-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
</div>
</div>
<div class="span5 pull-left">
<h5>Added dashboards</h5>
<table class="grafana-options-table">
<tr ng-repeat="playlistItem in playlistItems">
<td style="white-space: nowrap;">
<tr ng-repeat="playlistItem in ctrl.playlistItems">
<td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_id'">
{{playlistItem.title}}
</td>
<td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_tag'">
<a class="search-result-tag label label-tag" tag-color-from-name="playlistItem.title">
<i class="fa fa-tag"></i>
<span>{{playlistItem.title}}</span>
</a>
</td>
<td style="text-align: right">
<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="movePlaylistItemUp(playlistItem)">
<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="ctrl.movePlaylistItemUp(playlistItem)">
<i class="fa fa-arrow-up"></i>
</button>
<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="movePlaylistItemDown(playlistItem)">
<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="ctrl.movePlaylistItemDown(playlistItem)">
<i class="fa fa-arrow-down"></i>
</button>
<button class="btn btn-inverse btn-mini" ng-click="removePlaylistItem(playlistItem)">
<button class="btn btn-inverse btn-mini" ng-click="ctrl.removePlaylistItem(playlistItem)">
<i class="fa fa-remove"></i>
</button>
</td>
@ -113,11 +119,11 @@
<!-- <div class="tight-form"> -->
<button type="button"
class="btn btn-success"
ng-disabled="playlistEditForm.$invalid || isPlaylistEmpty()"
ng-click="savePlaylist(playlist, playlistItems)">Save</button>
ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()"
ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Save</button>
<button type="button"
class="btn btn-inverse"
ng-click="backToList()">Cancel</button>
ng-click="ctrl.backToList()">Cancel</button>
<!-- </div> -->
</div>

View File

@ -0,0 +1,26 @@
<div class="playlist-search-field-wrapper">
<span style="position: relative;">
<input type="text" placeholder="Find dashboards by name" tabindex="1"
ng-keydown="ctrl.keyDown($event)" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.searchDashboards()" />
</span>
<div class="playlist-search-switches">
<i class="fa fa-filter"></i>
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.showStarred()" tabindex="2">
<i class="fa fa-remove" ng-show="ctrl.query.starred"></i>
starred
</a> |
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.getTags()" tabindex="3">
<i class="fa fa-remove" ng-show="ctrl.tagsMode"></i>
tags
</a>
<span ng-if="ctrl.query.tag.length">
|
<span ng-repeat="tagName in ctrl.query.tag">
<a ng-click="ctrl.removeTag(tagName, $event)" tag-color-from-name="ctrl.tagName" class="label label-tag">
<i class="fa fa-remove"></i>
{{tagName}}
</a>
</span>
</span>
</div>
</div>

View File

@ -19,7 +19,7 @@
<th style="width: 25px"></th>
</thead>
<tr ng-repeat="playlist in playlists">
<tr ng-repeat="playlist in ctrl.playlists">
<td>
<a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
</td>
@ -39,7 +39,7 @@
</a>
</td>
<td class="text-right">
<a ng-click="removePlaylist(playlist)" class="btn btn-danger btn-mini">
<a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>

Some files were not shown because too many files have changed in this diff Show More