Просмотр исходного кода

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

Torkel Ödegaard 10 лет назад
Родитель
Сommit
f188900379

+ 1 - 1
docs/sources/reference/http_api.md

@@ -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**:

+ 8 - 1
pkg/api/api.go

@@ -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) {
 			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))

+ 163 - 8
pkg/api/cloudwatch/metrics.go

@@ -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 (
 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() {
 		"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) {
 
 
 	json.Unmarshal(req.Body, reqParam)
 	json.Unmarshal(req.Body, reqParam)
 
 
-	namespaceMetrics, exists := metricsMap[reqParam.Parameters.Namespace]
-	if !exists {
-		c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
-		return
+	var namespaceMetrics []string
+	if !isCustomMetrics(reqParam.Parameters.Namespace) {
+		var exists bool
+		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) {
 
 
 	json.Unmarshal(req.Body, reqParam)
 	json.Unmarshal(req.Body, reqParam)
 
 
-	dimensionValues, exists := dimensionsMap[reqParam.Parameters.Namespace]
-	if !exists {
-		c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
-		return
+	var dimensionValues []string
+	if !isCustomMetrics(reqParam.Parameters.Namespace) {
+		var exists bool
+		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) {
 
 
 	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
+}

+ 63 - 0
pkg/api/cloudwatch/metrics_test.go

@@ -0,0 +1,63 @@
+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")
+		})
+	})
+
+}

+ 22 - 13
pkg/api/dashboard.go

@@ -49,17 +49,13 @@ func GetDashboard(c *middleware.Context) {
 
 
 	dash := query.Result
 	dash := query.Result
 
 
-	// Finding the last updater of the dashboard
-	updater := "Anonymous"
-	if dash.UpdatedBy != 0 {
-		userQuery := m.GetUserByIdQuery{Id: dash.UpdatedBy}
-		userErr := bus.Dispatch(&userQuery)
-		if userErr != nil {
-			updater = "Unknown"
-		} else {
-			user := userQuery.Result
-			updater = user.Login
-		}
+	// Finding creator and last updater of the dashboard
+	updater, creator := "Anonymous", "Anonymous"
+	if dash.UpdatedBy > 0 {
+		updater = getUserLogin(dash.UpdatedBy)
+	}
+	if dash.CreatedBy > 0 {
+		creator = getUserLogin(dash.CreatedBy)
 	}
 	}
 
 
 	dto := dtos.DashboardFullWithMeta{
 	dto := dtos.DashboardFullWithMeta{
@@ -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) {
 	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()

+ 40 - 1
pkg/api/dashboard_snapshot.go

@@ -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) {
 
 
 	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)
+}

+ 2 - 0
pkg/api/dtos/models.go

@@ -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 {

+ 6 - 0
pkg/api/index.go

@@ -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",

+ 26 - 0
pkg/models/dashboard_snapshot.go

@@ -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 {
 
 
 	Result *DashboardSnapshot
 	Result *DashboardSnapshot
 }
 }
+
+type DashboardSnapshots []*DashboardSnapshot
+
+type GetDashboardSnapshotsQuery struct {
+	Name  string
+	Limit int
+	OrgId int64
+
+	Result DashboardSnapshots
+}

+ 7 - 3
pkg/models/dashboards.go

@@ -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 {
 // 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() {
 
 
 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
 }
 }

+ 16 - 0
pkg/services/sqlstore/dashboard_snapshot.go

@@ -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 {
 	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
+}

+ 5 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -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,
+	}))
 }
 }

+ 5 - 0
public/app/core/routes/all.js

@@ -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',

+ 1 - 0
public/app/features/all.js

@@ -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',

+ 29 - 7
public/app/features/dashboard/partials/settings.html

@@ -115,9 +115,9 @@
 	</div>
 	</div>
 
 
   <div ng-if="editor.index == 4">
   <div ng-if="editor.index == 4">
-    <div class="editor-row">
-      <div class="tight-form-section">
-        <h5>Dashboard info</h5>
+    <div class="row">
+      <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">
@@ -129,6 +129,17 @@
           </ul>
           </ul>
           <div class="clearfix"></div>
           <div class="clearfix"></div>
         </div>
         </div>
+        <div class="tight-form">
+          <ul class="tight-form-list">
+            <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">
         <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">
@@ -136,17 +147,28 @@
             </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>

+ 1 - 0
public/app/features/snapshot/all.ts

@@ -0,0 +1 @@
+import './snapshot_ctrl';

+ 39 - 0
public/app/features/snapshot/partials/snapshots.html

@@ -0,0 +1,39 @@
+<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>

+ 42 - 0
public/app/features/snapshot/snapshot_ctrl.ts

@@ -0,0 +1,42 @@
+///<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);

+ 8 - 6
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -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) {
         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?([^,]+?)\)/);

+ 2 - 2
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js

@@ -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, _) {
     };
     };
 
 
     $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));
     };
     };