Commit f1889003 by Torkel Ödegaard

Merge branch 'master' of github.com:grafana/grafana

parents f80d5f8c fcc960e9
...@@ -75,7 +75,7 @@ Creates a new dashboard or updates an existing dashboard. ...@@ -75,7 +75,7 @@ Creates a new dashboard or updates an existing dashboard.
JSON Body schema: JSON Body schema:
- **dashboard** – The complete dashboard model, id = null to create a new dashboard - **dashboard** – The complete dashboard model, id = null to create a new dashboard.
- **overwrite** – Set to true if you want to overwrite existing dashboard with newer version or with same dashboard title. - **overwrite** – Set to true if you want to overwrite existing dashboard with newer version or with same dashboard title.
**Example Response**: **Example Response**:
......
...@@ -69,9 +69,11 @@ func Register(r *macaron.Macaron) { ...@@ -69,9 +69,11 @@ func Register(r *macaron.Macaron) {
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword)) r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
// dashboard snapshots // dashboard snapshots
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/dashboard/snapshot/*", Index) r.Get("/dashboard/snapshot/*", Index)
r.Get("/dashboard/snapshots/", reqSignedIn, Index)
// api for dashboard snapshots
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/api/snapshot/shared-options/", GetSharingOptions) r.Get("/api/snapshot/shared-options/", GetSharingOptions)
r.Get("/api/snapshots/:key", GetDashboardSnapshot) r.Get("/api/snapshots/:key", GetDashboardSnapshot)
r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot) r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
...@@ -183,6 +185,11 @@ func Register(r *macaron.Macaron) { ...@@ -183,6 +185,11 @@ func Register(r *macaron.Macaron) {
r.Get("/tags", GetDashboardTags) r.Get("/tags", GetDashboardTags)
}) })
// Dashboard snapshots
r.Group("/dashboard/snapshots", func() {
r.Get("/", wrap(SearchDashboardSnapshots))
})
// Playlist // Playlist
r.Group("/playlists", func() { r.Group("/playlists", func() {
r.Get("/", wrap(SearchPlaylists)) r.Get("/", wrap(SearchPlaylists))
......
...@@ -3,7 +3,14 @@ package cloudwatch ...@@ -3,7 +3,14 @@ package cloudwatch
import ( import (
"encoding/json" "encoding/json"
"sort" "sort"
"strings"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
...@@ -11,6 +18,14 @@ import ( ...@@ -11,6 +18,14 @@ import (
var metricsMap map[string][]string var metricsMap map[string][]string
var dimensionsMap map[string][]string var dimensionsMap map[string][]string
type CustomMetricsCache struct {
Expire time.Time
Cache []string
}
var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache
var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache
func init() { func init() {
metricsMap = map[string][]string{ metricsMap = map[string][]string{
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, "AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
...@@ -85,6 +100,9 @@ func init() { ...@@ -85,6 +100,9 @@ func init() {
"AWS/WAF": {"Rule", "WebACL"}, "AWS/WAF": {"Rule", "WebACL"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"}, "AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
} }
customMetricsMetricsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
customMetricsDimensionsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
} }
// Whenever this list is updated, frontend list should also be updated. // Whenever this list is updated, frontend list should also be updated.
...@@ -127,10 +145,19 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) { ...@@ -127,10 +145,19 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
json.Unmarshal(req.Body, reqParam) json.Unmarshal(req.Body, reqParam)
namespaceMetrics, exists := metricsMap[reqParam.Parameters.Namespace] var namespaceMetrics []string
if !exists { if !isCustomMetrics(reqParam.Parameters.Namespace) {
c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil) var exists bool
return if namespaceMetrics, exists = metricsMap[reqParam.Parameters.Namespace]; !exists {
c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
return
}
} else {
var err error
if namespaceMetrics, err = getMetricsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, getAllMetrics); err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
} }
sort.Sort(sort.StringSlice(namespaceMetrics)) sort.Sort(sort.StringSlice(namespaceMetrics))
...@@ -151,10 +178,19 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) { ...@@ -151,10 +178,19 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
json.Unmarshal(req.Body, reqParam) json.Unmarshal(req.Body, reqParam)
dimensionValues, exists := dimensionsMap[reqParam.Parameters.Namespace] var dimensionValues []string
if !exists { if !isCustomMetrics(reqParam.Parameters.Namespace) {
c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil) var exists bool
return if dimensionValues, exists = dimensionsMap[reqParam.Parameters.Namespace]; !exists {
c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
return
}
} else {
var err error
if dimensionValues, err = getDimensionsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, getAllMetrics); err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
} }
sort.Sort(sort.StringSlice(dimensionValues)) sort.Sort(sort.StringSlice(dimensionValues))
...@@ -165,3 +201,122 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) { ...@@ -165,3 +201,122 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
c.JSON(200, result) c.JSON(200, result)
} }
func getAllMetrics(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
cfg := &aws.Config{
Region: aws.String(region),
Credentials: getCredentials(database),
}
svc := cloudwatch.New(session.New(cfg), cfg)
params := &cloudwatch.ListMetricsInput{
Namespace: aws.String(namespace),
}
var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
for _, metric := range metrics {
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
}
return !lastPage
})
if err != nil {
return resp, err
}
return resp, nil
}
var metricsCacheLock sync.Mutex
func getMetricsForCustomMetrics(region string, namespace string, database string, getAllMetrics func(string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(region, namespace, database)
if err != nil {
return []string{}, err
}
metricsCacheLock.Lock()
defer metricsCacheLock.Unlock()
if _, ok := customMetricsMetricsMap[database]; !ok {
customMetricsMetricsMap[database] = make(map[string]map[string]*CustomMetricsCache)
}
if _, ok := customMetricsMetricsMap[database][region]; !ok {
customMetricsMetricsMap[database][region] = make(map[string]*CustomMetricsCache)
}
if _, ok := customMetricsMetricsMap[database][region][namespace]; !ok {
customMetricsMetricsMap[database][region][namespace] = &CustomMetricsCache{}
customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
}
if customMetricsMetricsMap[database][region][namespace].Expire.After(time.Now()) {
return customMetricsMetricsMap[database][region][namespace].Cache, nil
}
customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
customMetricsMetricsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
for _, metric := range result.Metrics {
if isDuplicate(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName) {
continue
}
customMetricsMetricsMap[database][region][namespace].Cache = append(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName)
}
return customMetricsMetricsMap[database][region][namespace].Cache, nil
}
var dimensionsCacheLock sync.Mutex
func getDimensionsForCustomMetrics(region string, namespace string, database string, getAllMetrics func(string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(region, namespace, database)
if err != nil {
return []string{}, err
}
dimensionsCacheLock.Lock()
defer dimensionsCacheLock.Unlock()
if _, ok := customMetricsDimensionsMap[database]; !ok {
customMetricsDimensionsMap[database] = make(map[string]map[string]*CustomMetricsCache)
}
if _, ok := customMetricsDimensionsMap[database][region]; !ok {
customMetricsDimensionsMap[database][region] = make(map[string]*CustomMetricsCache)
}
if _, ok := customMetricsDimensionsMap[database][region][namespace]; !ok {
customMetricsDimensionsMap[database][region][namespace] = &CustomMetricsCache{}
customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
}
if customMetricsDimensionsMap[database][region][namespace].Expire.After(time.Now()) {
return customMetricsDimensionsMap[database][region][namespace].Cache, nil
}
customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
customMetricsDimensionsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
for _, metric := range result.Metrics {
for _, dimension := range metric.Dimensions {
if isDuplicate(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name) {
continue
}
customMetricsDimensionsMap[database][region][namespace].Cache = append(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name)
}
}
return customMetricsDimensionsMap[database][region][namespace].Cache, nil
}
func isDuplicate(nameList []string, target string) bool {
for _, name := range nameList {
if name == target {
return true
}
}
return false
}
func isCustomMetrics(namespace string) bool {
return strings.Index(namespace, "AWS/") != 0
}
package cloudwatch
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
. "github.com/smartystreets/goconvey/convey"
)
func TestCloudWatchMetrics(t *testing.T) {
Convey("When calling getMetricsForCustomMetrics", t, func() {
region := "us-east-1"
namespace := "Foo"
database := "default"
f := func(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
MetricName: aws.String("Test_MetricName"),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("Test_DimensionName"),
},
},
},
},
}, nil
}
metrics, _ := getMetricsForCustomMetrics(region, namespace, database, f)
Convey("Should contain Test_MetricName", func() {
So(metrics, ShouldContain, "Test_MetricName")
})
})
Convey("When calling getDimensionsForCustomMetrics", t, func() {
region := "us-east-1"
namespace := "Foo"
database := "default"
f := func(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
MetricName: aws.String("Test_MetricName"),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("Test_DimensionName"),
},
},
},
},
}, nil
}
dimensionKeys, _ := getDimensionsForCustomMetrics(region, namespace, database, f)
Convey("Should contain Test_DimensionName", func() {
So(dimensionKeys, ShouldContain, "Test_DimensionName")
})
})
}
...@@ -49,17 +49,13 @@ func GetDashboard(c *middleware.Context) { ...@@ -49,17 +49,13 @@ func GetDashboard(c *middleware.Context) {
dash := query.Result dash := query.Result
// Finding the last updater of the dashboard // Finding creator and last updater of the dashboard
updater := "Anonymous" updater, creator := "Anonymous", "Anonymous"
if dash.UpdatedBy != 0 { if dash.UpdatedBy > 0 {
userQuery := m.GetUserByIdQuery{Id: dash.UpdatedBy} updater = getUserLogin(dash.UpdatedBy)
userErr := bus.Dispatch(&userQuery) }
if userErr != nil { if dash.CreatedBy > 0 {
updater = "Unknown" creator = getUserLogin(dash.CreatedBy)
} else {
user := userQuery.Result
updater = user.Login
}
} }
dto := dtos.DashboardFullWithMeta{ dto := dtos.DashboardFullWithMeta{
...@@ -74,12 +70,25 @@ func GetDashboard(c *middleware.Context) { ...@@ -74,12 +70,25 @@ func GetDashboard(c *middleware.Context) {
Created: dash.Created, Created: dash.Created,
Updated: dash.Updated, Updated: dash.Updated,
UpdatedBy: updater, UpdatedBy: updater,
CreatedBy: creator,
Version: dash.Version,
}, },
} }
c.JSON(200, dto) c.JSON(200, dto)
} }
func getUserLogin(userId int64) string {
query := m.GetUserByIdQuery{Id: userId}
err := bus.Dispatch(&query)
if err != nil {
return "Anonymous"
} else {
user := query.Result
return user.Login
}
}
func DeleteDashboard(c *middleware.Context) { func DeleteDashboard(c *middleware.Context) {
slug := c.Params(":slug") slug := c.Params(":slug")
...@@ -104,9 +113,9 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { ...@@ -104,9 +113,9 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
if !c.IsSignedIn { if !c.IsSignedIn {
cmd.UpdatedBy = 0 cmd.UserId = -1
} else { } else {
cmd.UpdatedBy = c.UserId cmd.UserId = c.UserId
} }
dash := cmd.GetDashboardModel() dash := cmd.GetDashboardModel()
......
...@@ -36,7 +36,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho ...@@ -36,7 +36,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
cmd.DeleteKey = util.GetRandomString(32) cmd.DeleteKey = util.GetRandomString(32)
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
cmd.UserId = c.UserId cmd.UserId = c.UserId
cmd.Name = c.Name
metrics.M_Api_Dashboard_Snapshot_Create.Inc(1) metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
} }
...@@ -99,3 +98,43 @@ func DeleteDashboardSnapshot(c *middleware.Context) { ...@@ -99,3 +98,43 @@ func DeleteDashboardSnapshot(c *middleware.Context) {
c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."}) c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
} }
func SearchDashboardSnapshots(c *middleware.Context) Response {
query := c.Query("query")
limit := c.QueryInt("limit")
if limit == 0 {
limit = 1000
}
searchQuery := m.GetDashboardSnapshotsQuery{
Name: query,
Limit: limit,
OrgId: c.OrgId,
}
err := bus.Dispatch(&searchQuery)
if err != nil {
return ApiError(500, "Search failed", err)
}
dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
for i, snapshot := range searchQuery.Result {
dtos[i] = &m.DashboardSnapshotDTO{
Id: snapshot.Id,
Name: snapshot.Name,
Key: snapshot.Key,
DeleteKey: snapshot.DeleteKey,
OrgId: snapshot.OrgId,
UserId: snapshot.UserId,
External: snapshot.External,
ExternalUrl: snapshot.ExternalUrl,
Expires: snapshot.Expires,
Created: snapshot.Created,
Updated: snapshot.Updated,
}
}
return Json(200, dtos)
//return Json(200, searchQuery.Result)
}
...@@ -42,6 +42,8 @@ type DashboardMeta struct { ...@@ -42,6 +42,8 @@ type DashboardMeta struct {
Created time.Time `json:"created"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"` UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
} }
type DashboardFullWithMeta struct { type DashboardFullWithMeta struct {
......
...@@ -60,6 +60,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { ...@@ -60,6 +60,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Url: "/playlists", Url: "/playlists",
}) })
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Snapshots",
Icon: "fa fa-fw fa-camera-retro",
Url: "/dashboard/snapshots",
})
if c.OrgRole == m.ROLE_ADMIN { if c.OrgRole == m.ROLE_ADMIN {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Data Sources", Text: "Data Sources",
......
...@@ -20,6 +20,22 @@ type DashboardSnapshot struct { ...@@ -20,6 +20,22 @@ type DashboardSnapshot struct {
Dashboard map[string]interface{} Dashboard map[string]interface{}
} }
// DashboardSnapshotDTO without dashboard map
type DashboardSnapshotDTO struct {
Id int64 `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
DeleteKey string `json:"deleteKey"`
OrgId int64 `json:"orgId"`
UserId int64 `json:"userId"`
External bool `json:"external"`
ExternalUrl string `json:"externalUrl"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ----------------- // -----------------
// COMMANDS // COMMANDS
...@@ -48,3 +64,13 @@ type GetDashboardSnapshotQuery struct { ...@@ -48,3 +64,13 @@ type GetDashboardSnapshotQuery struct {
Result *DashboardSnapshot Result *DashboardSnapshot
} }
type DashboardSnapshots []*DashboardSnapshot
type GetDashboardSnapshotsQuery struct {
Name string
Limit int
OrgId int64
Result DashboardSnapshots
}
...@@ -34,6 +34,7 @@ type Dashboard struct { ...@@ -34,6 +34,7 @@ type Dashboard struct {
Updated time.Time Updated time.Time
UpdatedBy int64 UpdatedBy int64
CreatedBy int64
Title string Title string
Data map[string]interface{} Data map[string]interface{}
...@@ -91,8 +92,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard { ...@@ -91,8 +92,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard {
// GetDashboardModel turns the command into the savable model // GetDashboardModel turns the command into the savable model
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash := NewDashboardFromJson(cmd.Dashboard) dash := NewDashboardFromJson(cmd.Dashboard)
if dash.Data["version"] == 0 {
dash.CreatedBy = cmd.UserId
}
dash.UpdatedBy = cmd.UserId
dash.OrgId = cmd.OrgId dash.OrgId = cmd.OrgId
dash.UpdatedBy = cmd.UpdatedBy
dash.UpdateSlug() dash.UpdateSlug()
return dash return dash
} }
...@@ -114,9 +118,9 @@ func (dash *Dashboard) UpdateSlug() { ...@@ -114,9 +118,9 @@ func (dash *Dashboard) UpdateSlug() {
type SaveDashboardCommand struct { type SaveDashboardCommand struct {
Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
Overwrite bool `json:"overwrite"` UserId int64 `json:"userId"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
UpdatedBy int64 `json:"-"` Overwrite bool `json:"overwrite"`
Result *Dashboard Result *Dashboard
} }
......
...@@ -12,6 +12,7 @@ func init() { ...@@ -12,6 +12,7 @@ func init() {
bus.AddHandler("sql", CreateDashboardSnapshot) bus.AddHandler("sql", CreateDashboardSnapshot)
bus.AddHandler("sql", GetDashboardSnapshot) bus.AddHandler("sql", GetDashboardSnapshot)
bus.AddHandler("sql", DeleteDashboardSnapshot) bus.AddHandler("sql", DeleteDashboardSnapshot)
bus.AddHandler("sql", SearchDashboardSnapshots)
} }
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error { func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
...@@ -64,3 +65,18 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error { ...@@ -64,3 +65,18 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
query.Result = &snapshot query.Result = &snapshot
return nil return nil
} }
func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error {
var snapshots = make(m.DashboardSnapshots, 0)
sess := x.Limit(query.Limit)
if query.Name != "" {
sess.Where("name LIKE ?", query.Name)
}
sess.Where("org_id = ?", query.OrgId)
err := sess.Find(&snapshots)
query.Result = snapshots
return err
}
...@@ -97,4 +97,9 @@ func addDashboardMigration(mg *Migrator) { ...@@ -97,4 +97,9 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add column updated_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{ mg.AddMigration("Add column updated_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
Name: "updated_by", Type: DB_Int, Nullable: true, Name: "updated_by", Type: DB_Int, Nullable: true,
})) }))
// add column to store creator of a dashboard
mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
Name: "created_by", Type: DB_Int, Nullable: true,
}))
} }
...@@ -137,6 +137,11 @@ define([ ...@@ -137,6 +137,11 @@ define([
templateUrl: 'public/app/partials/reset_password.html', templateUrl: 'public/app/partials/reset_password.html',
controller : 'ResetPasswordCtrl', controller : 'ResetPasswordCtrl',
}) })
.when('/dashboard/snapshots', {
templateUrl: 'app/features/snapshot/partials/snapshots.html',
controller : 'SnapshotsCtrl',
controllerAs: 'ctrl',
})
.when('/apps', { .when('/apps', {
templateUrl: 'public/app/features/apps/partials/list.html', templateUrl: 'public/app/features/apps/partials/list.html',
controller: 'AppListCtrl', controller: 'AppListCtrl',
......
...@@ -5,6 +5,7 @@ define([ ...@@ -5,6 +5,7 @@ define([
'./templating/templateSrv', './templating/templateSrv',
'./dashboard/all', './dashboard/all',
'./playlist/all', './playlist/all',
'./snapshot/all',
'./panel/all', './panel/all',
'./profile/profileCtrl', './profile/profileCtrl',
'./profile/changePasswordCtrl', './profile/changePasswordCtrl',
......
...@@ -115,9 +115,9 @@ ...@@ -115,9 +115,9 @@
</div> </div>
<div ng-if="editor.index == 4"> <div ng-if="editor.index == 4">
<div class="editor-row"> <div class="row">
<div class="tight-form-section"> <h5>Dashboard info</h5>
<h5>Dashboard info</h5> <div class="pull-left tight-form">
<div class="tight-form"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px"> <li class="tight-form-item" style="width: 120px">
...@@ -132,21 +132,43 @@ ...@@ -132,21 +132,43 @@
<div class="tight-form"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px"> <li class="tight-form-item" style="width: 120px">
Last updated by:
</li>
<li class="tight-form-item" style="width: 180px">
{{dashboardMeta.updatedBy}}
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Created at: Created at:
</li> </li>
<li class="tight-form-item" style="width: 180px"> <li class="tight-form-item" style="width: 180px">
{{formatDate(dashboardMeta.created)}} {{formatDate(dashboardMeta.created)}}
</li> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="tight-form last"> <div class="tight-form">
<ul class="tight-form-list"> <ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px"> <li class="tight-form-item" style="width: 120px">
Last updated by: Created by:
</li> </li>
<li class="tight-form-item" style="width: 180px"> <li class="tight-form-item" style="width: 180px">
{{dashboardMeta.updatedBy}} {{dashboardMeta.createdBy}}
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Current version:
</li>
<li class="tight-form-item" style="width: 180px">
{{dashboardMeta.version}}
</li> </li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
......
import './snapshot_ctrl';
<navbar icon="fa fa-fw fa-camera-retro" title="Dashboard snapshots"></navbar>
<div class="page-container">
<div class="page-wide">
<h2>Available snapshots</h2>
<table class="filter-table" style="margin-top: 20px">
<thead>
<th><strong>Name</strong></th>
<th><strong>Snapshot url</strong></th>
<th style="width: 70px"></th>
<th style="width: 25px"></th>
</thead>
<tr ng-repeat="snapshot in ctrl.snapshots">
<td>
<a href="dashboard/snapshot/{{snapshot.key}}">{{snapshot.name}}</a>
</td>
<td >
<a href="dashboard/snapshot/{{snapshot.key}}">dashboard/snapshot/{{snapshot.key}}</a>
</td>
<td class="text-center">
<a href="dashboard/snapshot/{{snapshot.key}}" class="btn btn-inverse btn-mini">
<i class="fa fa-eye"></i>
View
</a>
</td>
<td class="text-right">
<a ng-click="ctrl.removeSnapshot(snapshot)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class SnapshotsCtrl {
snapshots: any;
/** @ngInject */
constructor(private $rootScope, private backendSrv) {
this.backendSrv.get('/api/dashboard/snapshots').then(result => {
this.snapshots = result;
});
}
removeSnapshotConfirmed(snapshot) {
_.remove(this.snapshots, {key: snapshot.key});
this.backendSrv.get('/api/snapshots-delete/' + snapshot.deleteKey)
.then(() => {
this.$rootScope.appEvent('alert-success', ['Snapshot deleted', '']);
}, () => {
this.$rootScope.appEvent('alert-error', ['Unable to delete snapshot', '']);
this.snapshots.push(snapshot);
});
}
removeSnapshot(snapshot) {
this.$rootScope.appEvent('confirm-modal', {
title: 'Confirm delete snapshot',
text: 'Are you sure you want to delete snapshot ' + snapshot.name + '?',
yesText: "Delete",
icon: "fa-warning",
onConfirm: () => {
this.removeSnapshotConfirmed(snapshot);
}
});
}
}
angular.module('grafana.controllers').controller('SnapshotsCtrl', SnapshotsCtrl);
...@@ -90,18 +90,20 @@ function (angular, _, moment, dateMath) { ...@@ -90,18 +90,20 @@ function (angular, _, moment, dateMath) {
return this.awsRequest({action: '__GetNamespaces'}); return this.awsRequest({action: '__GetNamespaces'});
}; };
this.getMetrics = function(namespace) { this.getMetrics = function(namespace, region) {
return this.awsRequest({ return this.awsRequest({
action: '__GetMetrics', action: '__GetMetrics',
region: region,
parameters: { parameters: {
namespace: templateSrv.replace(namespace) namespace: templateSrv.replace(namespace)
} }
}); });
}; };
this.getDimensionKeys = function(namespace) { this.getDimensionKeys = function(namespace, region) {
return this.awsRequest({ return this.awsRequest({
action: '__GetDimensions', action: '__GetDimensions',
region: region,
parameters: { parameters: {
namespace: templateSrv.replace(namespace) namespace: templateSrv.replace(namespace)
} }
...@@ -164,14 +166,14 @@ function (angular, _, moment, dateMath) { ...@@ -164,14 +166,14 @@ function (angular, _, moment, dateMath) {
return this.getNamespaces(); return this.getNamespaces();
} }
var metricNameQuery = query.match(/^metrics\(([^\)]+?)\)/); var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
if (metricNameQuery) { if (metricNameQuery) {
return this.getMetrics(metricNameQuery[1]); return this.getMetrics(metricNameQuery[1], metricNameQuery[3]);
} }
var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)\)/); var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
if (dimensionKeysQuery) { if (dimensionKeysQuery) {
return this.getDimensionKeys(dimensionKeysQuery[1]); return this.getDimensionKeys(dimensionKeysQuery[1], dimensionKeysQuery[3]);
} }
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/); var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
......
...@@ -102,7 +102,7 @@ function (angular, _) { ...@@ -102,7 +102,7 @@ function (angular, _) {
var query = $q.when([]); var query = $q.when([]);
if (segment.type === 'key' || segment.type === 'plus-button') { if (segment.type === 'key' || segment.type === 'plus-button') {
query = $scope.datasource.getDimensionKeys($scope.target.namespace); query = $scope.datasource.getDimensionKeys($scope.target.namespace, $scope.target.region);
} else if (segment.type === 'value') { } else if (segment.type === 'value') {
var dimensionKey = $scope.dimSegments[$index-2].value; var dimensionKey = $scope.dimSegments[$index-2].value;
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {}); query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
...@@ -160,7 +160,7 @@ function (angular, _) { ...@@ -160,7 +160,7 @@ function (angular, _) {
}; };
$scope.getMetrics = function() { $scope.getMetrics = function() {
return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ')') return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ',' + $scope.target.region + ')')
.then($scope.transformToSegments(true)); .then($scope.transformToSegments(true));
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment