mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
Merge branch 'master' of https://github.com/grafana/grafana into metadata
This commit is contained in:
commit
338afc80d5
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@ awsconfig
|
||||
/dist
|
||||
/emails/dist
|
||||
/public_gen
|
||||
/public/vendor/npm
|
||||
/tmp
|
||||
vendor/phantomjs/phantomjs
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
88
pkg/api/playlist_play.go
Normal 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
|
||||
}
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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:"-"`
|
||||
|
@ -150,3 +150,8 @@ type GetDashboardTagsQuery struct {
|
||||
OrgId int64
|
||||
Result []*DashboardTagCloudItem
|
||||
}
|
||||
|
||||
type GetDashboardsQuery struct {
|
||||
DashboardIds []int64
|
||||
Result *[]Dashboard
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
44
pkg/services/sqlstore/playlist_test.go
Normal file
44
pkg/services/sqlstore/playlist_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -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
66
pkg/util/encryption.go
Normal 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)
|
||||
}
|
27
pkg/util/encryption_test.go
Normal file
27
pkg/util/encryption_test.go
Normal 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")
|
||||
})
|
||||
|
||||
}
|
@ -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
46
pkg/util/url_test.go
Normal 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/")
|
||||
})
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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()">
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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()">
|
||||
|
@ -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) {
|
||||
|
@ -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"> (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>
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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() {
|
||||
|
@ -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'});
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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">  <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>
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
}
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -1,9 +1,8 @@
|
||||
define([
|
||||
'./panel_menu',
|
||||
'./panel_directive',
|
||||
'./panel_srv',
|
||||
'./panel_helper',
|
||||
'./solo_panel_ctrl',
|
||||
'./panel_loader',
|
||||
'./query_editor',
|
||||
'./panel_editor_tab',
|
||||
], function () {});
|
||||
|
234
public/app/features/panel/metrics_panel_ctrl.ts
Normal file
234
public/app/features/panel/metrics_panel_ctrl.ts
Normal 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};
|
51
public/app/features/panel/panel.ts
Normal file
51
public/app/features/panel/panel.ts
Normal 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,
|
||||
}
|
179
public/app/features/panel/panel_ctrl.ts
Normal file
179
public/app/features/panel/panel_ctrl.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
28
public/app/features/panel/panel_editor_tab.ts
Normal file
28
public/app/features/panel/panel_editor_tab.ts
Normal 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);
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -1,7 +0,0 @@
|
||||
define([
|
||||
'./panel_meta2',
|
||||
],
|
||||
function (panelMeta) {
|
||||
'use strict';
|
||||
return panelMeta.default;
|
||||
});
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -39,13 +39,8 @@ function (angular, $) {
|
||||
}
|
||||
|
||||
$scope.panel.span = 12;
|
||||
$scope.dashboardViewState = {registerPanel: function() { }, state: {}};
|
||||
};
|
||||
|
||||
if (!$scope.skipAutoInit) {
|
||||
$scope.init();
|
||||
}
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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() {
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
define([
|
||||
'./playlists_ctrl',
|
||||
'./playlist_search',
|
||||
'./playlist_srv',
|
||||
'./playlist_edit_ctrl',
|
||||
'./playlist_routes'
|
||||
|
@ -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}} ({{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>
|
||||
|
||||
|
26
public/app/features/playlist/partials/playlist_search.html
Normal file
26
public/app/features/playlist/partials/playlist_search.html
Normal 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>
|
@ -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
Loading…
Reference in New Issue
Block a user