Merge branch 'master' into influxdb-policy-selector

This commit is contained in:
Torkel Ödegaard 2016-01-18 18:10:01 +01:00
commit 3b5a583903
33 changed files with 358 additions and 297 deletions

View File

@ -5,3 +5,6 @@ if [ $? -gt 0 ]; then
echo "Some files aren't formatted, please run 'go fmt ./pkg/...' to format your source code before committing" echo "Some files aren't formatted, please run 'go fmt ./pkg/...' to format your source code before committing"
exit 1 exit 1
fi fi
grunt test

View File

@ -25,7 +25,7 @@ Name | Description
------------ | ------------- ------------ | -------------
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards. Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
Default | Default data source means that it will be pre-selected for new panels. Default | Default data source means that it will be pre-selected for new panels.
Url | The http protocol, ip and port of you graphite-web or graphite-api install. Url | The http protocol, ip and port of your graphite-web or graphite-api install.
Access | Proxy = access via Grafana backend, Direct = access directory from browser. Access | Proxy = access via Grafana backend, Direct = access directory from browser.

View File

@ -189,8 +189,8 @@ func Register(r *macaron.Macaron) {
r.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems)) r.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
r.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards)) r.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards))
r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist)) r.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist))
r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistQuery{}), ValidateOrgPlaylist, wrap(UpdatePlaylist)) r.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, wrap(UpdatePlaylist))
r.Post("/", reqEditorRole, bind(m.CreatePlaylistQuery{}), wrap(CreatePlaylist)) r.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), wrap(CreatePlaylist))
}) })
// Search // Search

View File

@ -2,11 +2,12 @@ package api
import ( import (
"errors" "errors"
"strconv"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"strconv"
) )
func ValidateOrgPlaylist(c *middleware.Context) { func ValidateOrgPlaylist(c *middleware.Context) {
@ -33,8 +34,8 @@ func SearchPlaylists(c *middleware.Context) Response {
limit = 1000 limit = 1000
} }
searchQuery := m.PlaylistQuery{ searchQuery := m.GetPlaylistsQuery{
Title: query, Name: query,
Limit: limit, Limit: limit,
OrgId: c.OrgId, OrgId: c.OrgId,
} }
@ -59,7 +60,7 @@ func GetPlaylist(c *middleware.Context) Response {
dto := &m.PlaylistDTO{ dto := &m.PlaylistDTO{
Id: cmd.Result.Id, Id: cmd.Result.Id,
Title: cmd.Result.Title, Name: cmd.Result.Name,
Interval: cmd.Result.Interval, Interval: cmd.Result.Interval,
OrgId: cmd.Result.OrgId, OrgId: cmd.Result.OrgId,
Items: playlistDTOs, Items: playlistDTOs,
@ -159,7 +160,7 @@ func GetPlaylistDashboards(c *middleware.Context) Response {
func DeletePlaylist(c *middleware.Context) Response { func DeletePlaylist(c *middleware.Context) Response {
id := c.ParamsInt64(":id") id := c.ParamsInt64(":id")
cmd := m.DeletePlaylistQuery{Id: id} cmd := m.DeletePlaylistCommand{Id: id, OrgId: c.OrgId}
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete playlist", err) return ApiError(500, "Failed to delete playlist", err)
} }
@ -167,28 +168,28 @@ func DeletePlaylist(c *middleware.Context) Response {
return Json(200, "") return Json(200, "")
} }
func CreatePlaylist(c *middleware.Context, query m.CreatePlaylistQuery) Response { func CreatePlaylist(c *middleware.Context, cmd m.CreatePlaylistCommand) Response {
query.OrgId = c.OrgId cmd.OrgId = c.OrgId
err := bus.Dispatch(&query)
if err != nil { if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to create playlist", err) return ApiError(500, "Failed to create playlist", err)
} }
return Json(200, query.Result) return Json(200, cmd.Result)
} }
func UpdatePlaylist(c *middleware.Context, query m.UpdatePlaylistQuery) Response { func UpdatePlaylist(c *middleware.Context, cmd m.UpdatePlaylistCommand) Response {
err := bus.Dispatch(&query) cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to save playlist", err)
}
playlistDTOs, err := LoadPlaylistItemDTOs(cmd.Id)
if err != nil { if err != nil {
return ApiError(500, "Failed to save playlist", err) return ApiError(500, "Failed to save playlist", err)
} }
playlistDTOs, err := LoadPlaylistItemDTOs(query.Id) cmd.Result.Items = playlistDTOs
if err != nil { return Json(200, cmd.Result)
return ApiError(500, "Failed to save playlist", err)
}
query.Result.Items = playlistDTOs
return Json(200, query.Result)
} }

View File

@ -3,6 +3,7 @@ package login
import ( import (
"errors" "errors"
"crypto/subtle"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -56,7 +57,7 @@ func loginUsingGrafanaDB(query *LoginUserQuery) error {
user := userQuery.Result user := userQuery.Result
passwordHashed := util.EncodePassword(query.Password, user.Salt) passwordHashed := util.EncodePassword(query.Password, user.Salt)
if passwordHashed != user.Password { if subtle.ConstantTimeCompare([]byte(passwordHashed), []byte(user.Password)) != 1 {
return ErrInvalidCredentials return ErrInvalidCredentials
} }

View File

@ -13,14 +13,14 @@ var (
// Playlist model // Playlist model
type Playlist struct { type Playlist struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Name string `json:"name"`
Interval string `json:"interval"` Interval string `json:"interval"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
} }
type PlaylistDTO struct { type PlaylistDTO struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Name string `json:"name"`
Interval string `json:"interval"` Interval string `json:"interval"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
Items []PlaylistItemDTO `json:"items"` Items []PlaylistItemDTO `json:"items"`
@ -71,35 +71,47 @@ type PlaylistDashboardDto struct {
// //
// COMMANDS // COMMANDS
// //
type PlaylistQuery struct {
Title string
Limit int
OrgId int64
Result Playlists type UpdatePlaylistCommand struct {
} OrgId int64 `json:"-"`
Id int64 `json:"id" binding:"Required"`
type UpdatePlaylistQuery struct { Name string `json:"name" binding:"Required"`
Id int64 Type string `json:"type"`
Title string Interval string `json:"interval"`
Type string Data []int64 `json:"data"`
Interval string Items []PlaylistItemDTO `json:"items"`
Items []PlaylistItemDTO
Result *PlaylistDTO Result *PlaylistDTO
} }
type CreatePlaylistQuery struct { type CreatePlaylistCommand struct {
Title string Name string `json:"name" binding:"Required"`
Type string Type string `json:"type"`
Interval string Interval string `json:"interval"`
Data []int64 Data []int64 `json:"data"`
OrgId int64 Items []PlaylistItemDTO `json:"items"`
Items []PlaylistItemDTO
OrgId int64 `json:"-"`
Result *Playlist Result *Playlist
} }
type DeletePlaylistCommand struct {
Id int64
OrgId int64
}
//
// QUERIES
//
type GetPlaylistsQuery struct {
Name string
Limit int
OrgId int64
Result Playlists
}
type GetPlaylistByIdQuery struct { type GetPlaylistByIdQuery struct {
Id int64 Id int64
Result *Playlist Result *Playlist
@ -114,7 +126,3 @@ type GetPlaylistDashboardsQuery struct {
DashboardIds []int64 DashboardIds []int64
Result *PlaylistDashboards Result *PlaylistDashboards
} }
type DeletePlaylistQuery struct {
Id int64
}

View File

@ -3,20 +3,23 @@ package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func addPlaylistMigrations(mg *Migrator) { func addPlaylistMigrations(mg *Migrator) {
playlistV1 := Table{ mg.AddMigration("Drop old table playlist table", NewDropTableMigration("playlist"))
mg.AddMigration("Drop old table playlist_item table", NewDropTableMigration("playlist_item"))
playlistV2 := Table{
Name: "playlist", Name: "playlist",
Columns: []*Column{ Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false}, {Name: "org_id", Type: DB_BigInt, Nullable: false},
}, },
} }
// create table // create table
mg.AddMigration("create playlist table v1", NewAddTableMigration(playlistV1)) mg.AddMigration("create playlist table v2", NewAddTableMigration(playlistV2))
playlistItemV1 := Table{ playlistItemV2 := Table{
Name: "playlist_item", Name: "playlist_item",
Columns: []*Column{ Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
@ -28,5 +31,5 @@ func addPlaylistMigrations(mg *Migrator) {
}, },
} }
mg.AddMigration("create playlist item table v1", NewAddTableMigration(playlistItemV1)) mg.AddMigration("create playlist item table v2", NewAddTableMigration(playlistItemV2))
} }

View File

@ -2,6 +2,7 @@ package sqlstore
import ( import (
"fmt" "fmt"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
@ -18,13 +19,13 @@ func init() {
bus.AddHandler("sql", GetPlaylistItem) bus.AddHandler("sql", GetPlaylistItem)
} }
func CreatePlaylist(query *m.CreatePlaylistQuery) error { func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
var err error var err error
playlist := m.Playlist{ playlist := m.Playlist{
Title: query.Title, Name: cmd.Name,
Interval: query.Interval, Interval: cmd.Interval,
OrgId: query.OrgId, OrgId: cmd.OrgId,
} }
_, err = x.Insert(&playlist) _, err = x.Insert(&playlist)
@ -32,7 +33,7 @@ func CreatePlaylist(query *m.CreatePlaylistQuery) error {
fmt.Printf("%v", playlist.Id) fmt.Printf("%v", playlist.Id)
playlistItems := make([]m.PlaylistItem, 0) playlistItems := make([]m.PlaylistItem, 0)
for _, item := range query.Items { for _, item := range cmd.Items {
playlistItems = append(playlistItems, m.PlaylistItem{ playlistItems = append(playlistItems, m.PlaylistItem{
PlaylistId: playlist.Id, PlaylistId: playlist.Id,
Type: item.Type, Type: item.Type,
@ -44,40 +45,40 @@ func CreatePlaylist(query *m.CreatePlaylistQuery) error {
_, err = x.Insert(&playlistItems) _, err = x.Insert(&playlistItems)
query.Result = &playlist cmd.Result = &playlist
return err return err
} }
func UpdatePlaylist(query *m.UpdatePlaylistQuery) error { func UpdatePlaylist(cmd *m.UpdatePlaylistCommand) error {
var err error var err error
x.Logger.SetLevel(5)
playlist := m.Playlist{ playlist := m.Playlist{
Id: query.Id, Id: cmd.Id,
Title: query.Title, OrgId: cmd.OrgId,
Interval: query.Interval, Name: cmd.Name,
Interval: cmd.Interval,
} }
existingPlaylist := x.Where("id = ?", query.Id).Find(m.Playlist{}) existingPlaylist := x.Where("id = ? AND org_id = ?", cmd.Id, cmd.OrgId).Find(m.Playlist{})
if existingPlaylist == nil { if existingPlaylist == nil {
return m.ErrPlaylistNotFound return m.ErrPlaylistNotFound
} }
query.Result = &m.PlaylistDTO{ cmd.Result = &m.PlaylistDTO{
Id: playlist.Id, Id: playlist.Id,
OrgId: playlist.OrgId, OrgId: playlist.OrgId,
Title: playlist.Title, Name: playlist.Name,
Interval: playlist.Interval, Interval: playlist.Interval,
} }
_, err = x.Id(query.Id).Cols("id", "title", "timespan").Update(&playlist) _, err = x.Id(cmd.Id).Cols("id", "name", "interval").Update(&playlist)
if err != nil { if err != nil {
return err return err
} }
rawSql := "DELETE FROM playlist_item WHERE playlist_id = ?" rawSql := "DELETE FROM playlist_item WHERE playlist_id = ?"
_, err = x.Exec(rawSql, query.Id) _, err = x.Exec(rawSql, cmd.Id)
if err != nil { if err != nil {
return err return err
@ -85,7 +86,7 @@ func UpdatePlaylist(query *m.UpdatePlaylistQuery) error {
playlistItems := make([]m.PlaylistItem, 0) playlistItems := make([]m.PlaylistItem, 0)
for _, item := range query.Items { for _, item := range cmd.Items {
playlistItems = append(playlistItems, m.PlaylistItem{ playlistItems = append(playlistItems, m.PlaylistItem{
PlaylistId: playlist.Id, PlaylistId: playlist.Id,
Type: item.Type, Type: item.Type,
@ -113,33 +114,33 @@ func GetPlaylist(query *m.GetPlaylistByIdQuery) error {
return err return err
} }
func DeletePlaylist(query *m.DeletePlaylistQuery) error { func DeletePlaylist(cmd *m.DeletePlaylistCommand) error {
if query.Id == 0 { if cmd.Id == 0 {
return m.ErrCommandValidationFailed return m.ErrCommandValidationFailed
} }
return inTransaction(func(sess *xorm.Session) error { return inTransaction(func(sess *xorm.Session) error {
var rawPlaylistSql = "DELETE FROM playlist WHERE id = ?" var rawPlaylistSql = "DELETE FROM playlist WHERE id = ? and org_id = ?"
_, err := sess.Exec(rawPlaylistSql, query.Id) _, err := sess.Exec(rawPlaylistSql, cmd.Id, cmd.OrgId)
if err != nil { if err != nil {
return err return err
} }
var rawItemSql = "DELETE FROM playlist_item WHERE playlist_id = ?" var rawItemSql = "DELETE FROM playlist_item WHERE playlist_id = ?"
_, err2 := sess.Exec(rawItemSql, query.Id) _, err2 := sess.Exec(rawItemSql, cmd.Id)
return err2 return err2
}) })
} }
func SearchPlaylists(query *m.PlaylistQuery) error { func SearchPlaylists(query *m.GetPlaylistsQuery) error {
var playlists = make(m.Playlists, 0) var playlists = make(m.Playlists, 0)
sess := x.Limit(query.Limit) sess := x.Limit(query.Limit)
if query.Title != "" { if query.Name != "" {
sess.Where("title LIKE ?", query.Title) sess.Where("name LIKE ?", query.Name)
} }
sess.Where("org_id = ?", query.OrgId) sess.Where("org_id = ?", query.OrgId)

View File

@ -139,7 +139,8 @@ export class GrafanaCtrl {
} }
} }
export function grafanaAppDirective() { /** @ngInject */
export function grafanaAppDirective(playlistSrv) {
return { return {
restrict: 'E', restrict: 'E',
controller: GrafanaCtrl, controller: GrafanaCtrl,
@ -165,22 +166,33 @@ export function grafanaAppDirective() {
// handle document clicks that should hide things // handle document clicks that should hide things
elem.click(function(evt) { elem.click(function(evt) {
if ($(evt.target).parents().length === 0) { var target = $(evt.target);
if (target.parents().length === 0) {
return; return;
} }
if (target.parents('.dash-playlist-actions').length === 0) {
playlistSrv.stop();
}
// hide search // hide search
if (elem.find('.search-container').length > 0) { if (elem.find('.search-container').length > 0) {
if ($(evt.target).parents('.search-container').length === 0) { if (target.parents('.search-container').length === 0) {
scope.appEvent('hide-dash-search'); scope.appEvent('hide-dash-search');
} }
} }
// hide sidemenu // hide sidemenu
if (!ignoreSideMenuHide && elem.find('.sidemenu').length > 0) { if (!ignoreSideMenuHide && elem.find('.sidemenu').length > 0) {
if ($(evt.target).parents('.sidemenu').length === 0) { if (target.parents('.sidemenu').length === 0) {
scope.$apply(() => scope.contextSrv.toggleSideMenu()); scope.$apply(() => scope.contextSrv.toggleSideMenu());
} }
} }
// hide popovers
var popover = elem.find('.popover');
if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
popover.hide();
}
}); });
} }
}; };

View File

@ -13,7 +13,7 @@
<span class="icon-circle top-nav-icon"> <span class="icon-circle top-nav-icon">
<i ng-class="ctrl.icon"></i> <i ng-class="ctrl.icon"></i>
</span> </span>
<a ng-href="{{ctl.titleUrl}}" class="top-nav-title"> <a ng-href="{{ctrl.titleUrl}}" class="top-nav-title">
{{ctrl.title}} {{ctrl.title}}
</a> </a>
<i ng-show="ctrl.subnav" class="top-nav-breadcrumb-icon fa fa-angle-right"></i> <i ng-show="ctrl.subnav" class="top-nav-breadcrumb-icon fa fa-angle-right"></i>

View File

@ -41,7 +41,7 @@ export class SideMenuCtrl {
this.orgMenu = [ this.orgMenu = [
{section: 'You', cssClass: 'dropdown-menu-title'}, {section: 'You', cssClass: 'dropdown-menu-title'},
{text: 'Preferences', url: this.getUrl('/profile')}, {text: 'Preferences', url: this.getUrl('/profile')},
{text: 'Account', url: this.getUrl('/profile')}, {text: 'Profile', url: this.getUrl('/profile')},
]; ];
if (this.isSignedIn) { if (this.isSignedIn) {

View File

@ -352,9 +352,15 @@ function($, _) {
kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3); kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3);
// Data Rate // Data Rate
kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps'); kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps');
kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps'); kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps');
kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bits', 1);
kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bits', 2);
kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bits', 3);
// Throughput // Throughput
kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops');
@ -595,6 +601,12 @@ function($, _) {
{text: 'packets/sec', value: 'pps'}, {text: 'packets/sec', value: 'pps'},
{text: 'bits/sec', value: 'bps'}, {text: 'bits/sec', value: 'bps'},
{text: 'bytes/sec', value: 'Bps'}, {text: 'bytes/sec', value: 'Bps'},
{text: 'kilobites/sec', value: 'Kbits'},
{text: 'kilobytes/sec', value: 'KBs'},
{text: 'megabites/sec', value: 'Mbits'},
{text: 'megabytes/sec', value: 'MBs'},
{text: 'gigabytes/sec', value: 'GBs'},
{text: 'gigabites/sec', value: 'Gbits'},
] ]
}, },
{ {

View File

@ -1,9 +1,8 @@
<topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true"> <navbar title="Apps" title-url="apps" icon="fa fa-fw fa-cubes" subnav="true">
<ul class="nav"> <ul class="nav">
<li ><a href="apps">Overview</a></li> <li class="active" ><a href="apps/edit/{{ctrl.current.type}}">{{ctrl.appModel.name}}</a></li>
<li class="active" ><a href="apps/edit/{{ctrl.current.type}}">Edit</a></li>
</ul> </ul>
</topnav> </navbar>
<div class="page-container"> <div class="page-container">
<div class="apps-side-box"> <div class="apps-side-box">

View File

@ -2,11 +2,11 @@ define([
'./dashboardCtrl', './dashboardCtrl',
'./dashboardLoaderSrv', './dashboardLoaderSrv',
'./dashnav/dashnav', './dashnav/dashnav',
'./submenu/submenu',
'./saveDashboardAsCtrl', './saveDashboardAsCtrl',
'./rowCtrl', './rowCtrl',
'./shareModalCtrl', './shareModalCtrl',
'./shareSnapshotCtrl', './shareSnapshotCtrl',
'./submenuCtrl',
'./dashboardSrv', './dashboardSrv',
'./keybindings', './keybindings',
'./viewStateSrv', './viewStateSrv',

View File

@ -0,0 +1,30 @@
<div class="submenu-controls">
<div class="tight-form borderless">
<ul class="tight-form-list" ng-if="ctrl.dashboard.templating.list.length > 0">
<li ng-repeat="variable in ctrl.variables" class="submenu-item">
<span class="template-variable tight-form-item" ng-show="!variable.hideLabel" style="padding-right: 5px">
{{variable.label || variable.name}}:
</span>
<value-select-dropdown variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
</li>
</ul>
<ul class="tight-form-list" ng-if="ctrl.dashboard.annotations.list.length > 0">
<li ng-repeat="annotation in ctrl.dashboard.annotations.list" class="submenu-item annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
<a ng-click="ctrl.disableAnnotation(annotation)">
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
{{annotation.name}}
<input class="cr1" id="hideYAxis" type="checkbox" ng-model="annotation.enable" ng-checked="annotation.enable">
<label for="hideYAxis" class="cr1"></label>
</a>
</li>
</ul>
<ul class="tight-form-list pull-right" ng-if="ctrl.dashboard.links.length > 0">
<dash-links-container links="ctrl.dashboard.links"></dash-links-container>
</ul>
<div class="clearfix"></div>
</div>
</div>

View File

@ -0,0 +1,46 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
export class SubmenuCtrl {
annotations: any;
variables: any;
dashboard: any;
constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;
}
disableAnnotation(annotation) {
annotation.enable = !annotation.enable;
this.$rootScope.$broadcast('refresh');
}
getValuesForTag(variable, tagKey) {
return this.templateValuesSrv.getValuesForTag(variable, tagKey);
}
variableUpdated(variable) {
this.templateValuesSrv.variableUpdated(variable).then(() => {
this.dynamicDashboardSrv.update(this.dashboard);
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
});
}
}
export function submenuDirective() {
return {
restrict: 'E',
templateUrl: 'app/features/dashboard/submenu/submenu.html',
controller: SubmenuCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
dashboard: "=",
}
};
}
angular.module('grafana.directives').directive('dashboardSubmenu', submenuDirective);

View File

@ -1,39 +0,0 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv, dynamicDashboardSrv) {
$scope.init = function() {
$scope.panel = $scope.pulldown;
$scope.row = $scope.pulldown;
$scope.annotations = $scope.dashboard.templating.list;
$scope.variables = $scope.dashboard.templating.list;
};
$scope.disableAnnotation = function (annotation) {
annotation.enable = !annotation.enable;
$rootScope.$broadcast('refresh');
};
$scope.getValuesForTag = function(variable, tagKey) {
return templateValuesSrv.getValuesForTag(variable, tagKey);
};
$scope.variableUpdated = function(variable) {
templateValuesSrv.variableUpdated(variable).then(function() {
dynamicDashboardSrv.update($scope.dashboard);
$rootScope.$emit('template-variable-value-updated');
$rootScope.$broadcast('refresh');
});
};
$scope.init();
});
});

View File

@ -1,6 +1,6 @@
define([ define([
'./playlists_ctrl', './playlists_ctrl',
'./playlistSrv', './playlist_srv',
'./playlist_edit_ctrl', './playlist_edit_ctrl',
'./playlist_routes' './playlist_routes'
], function () {}); ], function () {});

View File

@ -1,7 +1,7 @@
<navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true"> <navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true">
<ul class="nav"> <ul class="nav">
<li ng-class="{active: isNew()}" ng-show="isNew()"><a href="datasources/create">New</a></li> <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.title}}</a></li> <li class="active" ng-show="!isNew()"><a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a></li>
</ul> </ul>
</navbar> </navbar>
@ -20,7 +20,7 @@
Name Name
</li> </li>
<li> <li>
<input type="text" required ng-model="playlist.title" class="input-xlarge tight-form-input"> <input type="text" required ng-model="playlist.name" class="input-xlarge tight-form-input">
</li> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -1,12 +1,12 @@
<topnav icon="fa fa-fw fa-list" title="Playlists"></topnav> <navbar icon="fa fa-fw fa-list" title="Playlists"></navbar>
<div class="page-container"> <div class="page-container">
<div class="page-wide"> <div class="page-wide">
<button type="submit" class="btn btn-inverse pull-right" ng-click="createPlaylist()"> <a class="btn btn-inverse pull-right" href="playlists/create">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
New playlist New playlist
</button> </a>
<h2>Saved playlists</h2> <h2>Saved playlists</h2>
@ -21,7 +21,7 @@
</thead> </thead>
<tr ng-repeat="playlist in playlists"> <tr ng-repeat="playlist in playlists">
<td> <td>
<a href="playlists/edit/{{playlist.id}}">{{playlist.title}}</a> <a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
</td> </td>
<td > <td >
<a href="playlists/play/{{playlist.id}}">playlists/play/{{playlist.id}}</a> <a href="playlists/play/{{playlist.id}}">playlists/play/{{playlist.id}}</a>

View File

@ -1,56 +0,0 @@
define([
'angular',
'lodash',
'app/core/utils/kbn',
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.service('playlistSrv', function($location, $rootScope, $timeout) {
var self = this;
this.next = function() {
$timeout.cancel(self.cancelPromise);
angular.element(window).unbind('resize');
var dash = self.dashboards[self.index % self.dashboards.length];
$location.url('dashboard/' + dash.uri);
self.index++;
self.cancelPromise = $timeout(self.next, self.interval);
};
this.prev = function() {
self.index = Math.max(self.index - 2, 0);
self.next();
};
this.start = function(dashboards, interval) {
self.stop();
self.index = 0;
self.interval = kbn.interval_to_ms(interval);
self.dashboards = dashboards;
$rootScope.playlistSrv = this;
self.cancelPromise = $timeout(self.next, self.interval);
self.next();
};
this.stop = function() {
self.index = 0;
if (self.cancelPromise) {
$timeout.cancel(self.cancelPromise);
}
$rootScope.playlistSrv = null;
};
});
});

View File

@ -13,7 +13,9 @@ function (angular, config, _) {
$scope.foundPlaylistItems = []; $scope.foundPlaylistItems = [];
$scope.searchQuery = ''; $scope.searchQuery = '';
$scope.loading = false; $scope.loading = false;
$scope.playlist = {}; $scope.playlist = {
interval: '10m',
};
$scope.playlistItems = []; $scope.playlistItems = [];
$scope.init = function() { $scope.init = function() {
@ -68,7 +70,6 @@ function (angular, config, _) {
$scope.playlistItems.push(playlistItem); $scope.playlistItems.push(playlistItem);
$scope.filterFoundPlaylistItems(); $scope.filterFoundPlaylistItems();
}; };
$scope.removePlaylistItem = function(playlistItem) { $scope.removePlaylistItem = function(playlistItem) {

View File

@ -23,19 +23,10 @@ function (angular) {
controller : 'PlaylistEditCtrl' controller : 'PlaylistEditCtrl'
}) })
.when('/playlists/play/:id', { .when('/playlists/play/:id', {
templateUrl: 'app/partials/dashboard.html',
controller : 'LoadDashboardCtrl',
resolve: { resolve: {
init: function(backendSrv, playlistSrv, $route) { init: function(playlistSrv, $route) {
var playlistId = $route.current.params.id; var playlistId = $route.current.params.id;
playlistSrv.start(playlistId);
return backendSrv.get('/api/playlists/' + playlistId)
.then(function(playlist) {
return backendSrv.get('/api/playlists/' + playlistId + '/dashboards')
.then(function(dashboards) {
playlistSrv.start(dashboards, playlist.interval);
});
});
} }
} }
}); });

View File

@ -0,0 +1,67 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import coreModule from '../../core/core_module';
import kbn from 'app/core/utils/kbn';
class PlaylistSrv {
private cancelPromise: any;
private dashboards: any;
private index: number;
private interval: any;
private playlistId: number;
/** @ngInject */
constructor(private $rootScope: any, private $location: any, private $timeout: any, private backendSrv: any) { }
next() {
this.$timeout.cancel(this.cancelPromise);
var playedAllDashboards = this.index > this.dashboards.length - 1;
if (playedAllDashboards) {
this.start(this.playlistId);
} else {
var dash = this.dashboards[this.index];
this.$location.url('dashboard/' + dash.uri);
this.index++;
this.cancelPromise = this.$timeout(() => this.next(), this.interval);
}
}
prev() {
this.index = Math.max(this.index - 2, 0);
this.next();
}
start(playlistId) {
this.stop();
this.index = 0;
this.playlistId = playlistId;
this.$rootScope.playlistSrv = this;
this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
this.dashboards = dashboards;
this.interval = kbn.interval_to_ms(playlist.interval);
this.next();
});
});
}
stop() {
this.index = 0;
this.playlistId = 0;
if (this.cancelPromise) {
this.$timeout.cancel(this.cancelPromise);
}
this.$rootScope.playlistSrv = null;
}
}
coreModule.service('playlistSrv', PlaylistSrv);

View File

@ -13,31 +13,31 @@ function (angular, _) {
$scope.playlists = result; $scope.playlists = result;
}); });
$scope.removePlaylist = function(playlist) { $scope.removePlaylistConfirmed = function(playlist) {
var modalScope = $scope.$new(true); _.remove($scope.playlists, {id: playlist.id});
modalScope.playlist = playlist; backendSrv.delete('/api/playlists/' + playlist.id)
modalScope.removePlaylist = function() { .then(function() {
modalScope.dismiss(); $scope.appEvent('alert-success', ['Playlist deleted', '']);
_.remove($scope.playlists, {id: playlist.id}); }, function() {
$scope.appEvent('alert-error', ['Unable to delete playlist', '']);
backendSrv.delete('/api/playlists/' + playlist.id) $scope.playlists.push(playlist);
.then(function() {
$scope.appEvent('alert-success', ['Playlist deleted', '']);
}, function() {
$scope.appEvent('alert-error', ['Unable to delete playlist', '']);
$scope.playlists.push(playlist);
});
};
$scope.appEvent('show-modal', {
src: './app/features/playlist/partials/playlist-remove.html',
scope: modalScope
}); });
}; };
$scope.createPlaylist = function() { $scope.removePlaylist = function(playlist) {
$location.path('/playlists/create');
$scope.appEvent('confirm-modal', {
title: 'Confirm delete playlist',
text: 'Are you sure you want to delete playlist ' + playlist.name + '?',
yesText: "Delete",
icon: "fa-warning",
onConfirm: function() {
$scope.removePlaylistConfirmed(playlist);
}
});
}; };
}); });
}); });

View File

@ -1,8 +1,8 @@
<topnav title="Profile" title-url="profile" icon="fa fa-user" subnav="true"> <navbar title="Profile" title-url="profile" icon="fa fa-fw fa-user" subnav="true">
<ul class="nav"> <ul class="nav">
<li class="active"><a href="profile/password">Change password</a></li> <li class="active"><a href="profile/password">Change password</a></li>
</ul> </ul>
</topnav> </navbar>
<div class="page-container"> <div class="page-container">
<div class="page"> <div class="page">
@ -47,7 +47,11 @@
</div> </div>
<br> <br>
<button type="submit" class="pull-right btn btn-success" ng-click="changePassword()">Change Password</button> <div class="pull-right">
<button type="submit" class="btn btn-success" ng-click="changePassword()">Change Password</button>
&nbsp;
<a class="btn btn-inverse" href="profile">Cancel</a>
</div>
</form> </form>
</div> </div>

View File

@ -1,10 +1,10 @@
<topnav title="Account" title-url="profile" icon="fa fa-fw fa-user"> <navbar title="Profile" title-url="profile" icon="fa fa-fw fa-user">
</topnav> </navbar>
<div class="page-container"> <div class="page-container">
<div class="page-wide"> <div class="page-wide">
<h1>Account & Preferences</h1> <h1>Profile</h1>
<section class="simple-box"> <section class="simple-box">
<h3 class="simple-box-header">Preferences</h3> <h3 class="simple-box-header">Preferences</h3>

View File

@ -6,8 +6,7 @@
<div dash-search-view></div> <div dash-search-view></div>
<div class="clearfix"></div> <div class="clearfix"></div>
<div ng-if="submenuEnabled" ng-include="'app/partials/submenu.html'"> <dashboard-submenu ng-if="submenuEnabled" dashboard="dashboard"></dashboard-submenu>
</div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -1,30 +0,0 @@
<div class="submenu-controls" ng-controller="SubmenuCtrl">
<div class="tight-form borderless">
<ul class="tight-form-list" ng-if="dashboard.templating.list.length > 0">
<li ng-repeat="variable in variables" class="submenu-item">
<span class="template-variable tight-form-item" ng-show="!variable.hideLabel" style="padding-right: 5px">
{{variable.label || variable.name}}:
</span>
<value-select-dropdown variable="variable" on-updated="variableUpdated(variable)" get-values-for-tag="getValuesForTag(variable, tagKey)"></value-select-dropdown>
</li>
</ul>
<ul class="tight-form-list" ng-if="dashboard.annotations.list.length > 0">
<li ng-repeat="annotation in dashboard.annotations.list" class="submenu-item annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
<a ng-click="disableAnnotation(annotation)">
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
{{annotation.name}}
<input class="cr1" id="hideYAxis" type="checkbox" ng-model="annotation.enable" ng-checked="annotation.enable">
<label for="hideYAxis" class="cr1"></label>
</a>
</li>
</ul>
<ul class="tight-form-list pull-right" ng-if="dashboard.links.length > 0">
<dash-links-container links="dashboard.links"></dash-links-container>
</ul>
<div class="clearfix"></div>
</div>
</div>

View File

@ -45,7 +45,7 @@ function (angular, _, $) {
popoverScope.series = seriesInfo; popoverScope.series = seriesInfo;
popoverSrv.show({ popoverSrv.show({
element: el, element: el,
templateUrl: 'app/plugins/panels/graph/legend.popover.html', templateUrl: 'app/plugins/panel/graph/legend.popover.html',
scope: popoverScope scope: popoverScope
}); });
} }

View File

@ -23,6 +23,7 @@ function (SingleStatCtrl, _, $) {
elem = inner; elem = inner;
$panelContainer = elem.parents('.panel-container'); $panelContainer = elem.parents('.panel-container');
firstRender = false; firstRender = false;
hookupDrilldownLinkTooltip();
} }
} }
@ -186,41 +187,44 @@ function (SingleStatCtrl, _, $) {
} }
} }
// drilldown link tooltip function hookupDrilldownLinkTooltip() {
var drilldownTooltip = $('<div id="tooltip" class="">hello</div>"'); // drilldown link tooltip
var drilldownTooltip = $('<div id="tooltip" class="">hello</div>"');
elem.mouseleave(function() { elem.mouseleave(function() {
if (panel.links.length === 0) { return;} if (panel.links.length === 0) { return;}
drilldownTooltip.detach(); drilldownTooltip.detach();
}); });
elem.click(function() { elem.click(function(evt) {
if (!linkInfo) { return; } if (!linkInfo) { return; }
// ignore title clicks in title
if ($(evt).parents('.panel-header').length > 0) { return; }
if (linkInfo.target === '_blank') { if (linkInfo.target === '_blank') {
var redirectWindow = window.open(linkInfo.href, '_blank'); var redirectWindow = window.open(linkInfo.href, '_blank');
redirectWindow.location; redirectWindow.location;
return; return;
} }
if (linkInfo.href.indexOf('http') === 0) { if (linkInfo.href.indexOf('http') === 0) {
window.location.href = linkInfo.href; window.location.href = linkInfo.href;
} else { } else {
$timeout(function() { $timeout(function() {
$location.url(linkInfo.href); $location.url(linkInfo.href);
}); });
} }
drilldownTooltip.detach(); drilldownTooltip.detach();
}); });
elem.mousemove(function(e) { elem.mousemove(function(e) {
if (!linkInfo) { return;} if (!linkInfo) { return;}
drilldownTooltip.text('click to go to: ' + linkInfo.title); drilldownTooltip.text('click to go to: ' + linkInfo.title);
drilldownTooltip.place_tt(e.pageX+20, e.pageY-15);
drilldownTooltip.place_tt(e.pageX+20, e.pageY-15); });
}); }
} }
}; };
} }

View File

@ -27,6 +27,7 @@
<body ng-cloak> <body ng-cloak>
<grafana-app> <grafana-app>
<aside class="sidemenu-wrapper"> <aside class="sidemenu-wrapper">
<sidemenu ng-if="contextSrv.sidemenu"></sidemenu> <sidemenu ng-if="contextSrv.sidemenu"></sidemenu>
</aside> </aside>

3
symlink_git_hooks.sh Executable file
View File

@ -0,0 +1,3 @@
#/bin/bash
ln -s .hooks/* .git/hooks/