mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of github.com:grafana/grafana
This commit is contained in:
@@ -75,7 +75,7 @@ Creates a new dashboard or updates an existing dashboard.
|
||||
|
||||
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.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@@ -69,9 +69,11 @@ func Register(r *macaron.Macaron) {
|
||||
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
|
||||
|
||||
// dashboard snapshots
|
||||
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||
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/snapshots/:key", GetDashboardSnapshot)
|
||||
r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
|
||||
@@ -183,6 +185,11 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/tags", GetDashboardTags)
|
||||
})
|
||||
|
||||
// Dashboard snapshots
|
||||
r.Group("/dashboard/snapshots", func() {
|
||||
r.Get("/", wrap(SearchDashboardSnapshots))
|
||||
})
|
||||
|
||||
// Playlist
|
||||
r.Group("/playlists", func() {
|
||||
r.Get("/", wrap(SearchPlaylists))
|
||||
|
||||
@@ -3,7 +3,14 @@ package cloudwatch
|
||||
import (
|
||||
"encoding/json"
|
||||
"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/util"
|
||||
)
|
||||
@@ -11,6 +18,14 @@ import (
|
||||
var metricsMap 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() {
|
||||
metricsMap = map[string][]string{
|
||||
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
|
||||
@@ -85,6 +100,9 @@ func init() {
|
||||
"AWS/WAF": {"Rule", "WebACL"},
|
||||
"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.
|
||||
@@ -127,10 +145,19 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
|
||||
|
||||
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))
|
||||
|
||||
@@ -151,10 +178,19 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
|
||||
|
||||
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))
|
||||
|
||||
@@ -165,3 +201,122 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
|
||||
|
||||
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
pkg/api/cloudwatch/metrics_test.go
Normal file
63
pkg/api/cloudwatch/metrics_test.go
Normal file
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
@@ -49,17 +49,13 @@ func GetDashboard(c *middleware.Context) {
|
||||
|
||||
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{
|
||||
@@ -74,12 +70,25 @@ func GetDashboard(c *middleware.Context) {
|
||||
Created: dash.Created,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: updater,
|
||||
CreatedBy: creator,
|
||||
Version: dash.Version,
|
||||
},
|
||||
}
|
||||
|
||||
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) {
|
||||
slug := c.Params(":slug")
|
||||
|
||||
@@ -104,9 +113,9 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if !c.IsSignedIn {
|
||||
cmd.UpdatedBy = 0
|
||||
cmd.UserId = -1
|
||||
} else {
|
||||
cmd.UpdatedBy = c.UserId
|
||||
cmd.UserId = c.UserId
|
||||
}
|
||||
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
@@ -36,7 +36,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
|
||||
cmd.DeleteKey = util.GetRandomString(32)
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.UserId = c.UserId
|
||||
cmd.Name = c.Name
|
||||
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."})
|
||||
}
|
||||
|
||||
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 {
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
||||
@@ -60,6 +60,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
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 {
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
Text: "Data Sources",
|
||||
|
||||
@@ -20,6 +20,22 @@ type DashboardSnapshot struct {
|
||||
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
|
||||
|
||||
@@ -48,3 +64,13 @@ type GetDashboardSnapshotQuery struct {
|
||||
|
||||
Result *DashboardSnapshot
|
||||
}
|
||||
|
||||
type DashboardSnapshots []*DashboardSnapshot
|
||||
|
||||
type GetDashboardSnapshotsQuery struct {
|
||||
Name string
|
||||
Limit int
|
||||
OrgId int64
|
||||
|
||||
Result DashboardSnapshots
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ type Dashboard struct {
|
||||
Updated time.Time
|
||||
|
||||
UpdatedBy int64
|
||||
CreatedBy int64
|
||||
|
||||
Title string
|
||||
Data map[string]interface{}
|
||||
@@ -91,8 +92,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard {
|
||||
// GetDashboardModel turns the command into the savable model
|
||||
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
||||
dash := NewDashboardFromJson(cmd.Dashboard)
|
||||
if dash.Data["version"] == 0 {
|
||||
dash.CreatedBy = cmd.UserId
|
||||
}
|
||||
dash.UpdatedBy = cmd.UserId
|
||||
dash.OrgId = cmd.OrgId
|
||||
dash.UpdatedBy = cmd.UpdatedBy
|
||||
dash.UpdateSlug()
|
||||
return dash
|
||||
}
|
||||
@@ -114,9 +118,9 @@ func (dash *Dashboard) UpdateSlug() {
|
||||
|
||||
type SaveDashboardCommand struct {
|
||||
Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
UserId int64 `json:"userId"`
|
||||
OrgId int64 `json:"-"`
|
||||
UpdatedBy int64 `json:"-"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
|
||||
Result *Dashboard
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ func init() {
|
||||
bus.AddHandler("sql", CreateDashboardSnapshot)
|
||||
bus.AddHandler("sql", GetDashboardSnapshot)
|
||||
bus.AddHandler("sql", DeleteDashboardSnapshot)
|
||||
bus.AddHandler("sql", SearchDashboardSnapshots)
|
||||
}
|
||||
|
||||
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
|
||||
@@ -64,3 +65,18 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
|
||||
query.Result = &snapshot
|
||||
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) {
|
||||
mg.AddMigration("Add column updated_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
|
||||
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([
|
||||
templateUrl: 'public/app/partials/reset_password.html',
|
||||
controller : 'ResetPasswordCtrl',
|
||||
})
|
||||
.when('/dashboard/snapshots', {
|
||||
templateUrl: 'app/features/snapshot/partials/snapshots.html',
|
||||
controller : 'SnapshotsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/apps', {
|
||||
templateUrl: 'public/app/features/apps/partials/list.html',
|
||||
controller: 'AppListCtrl',
|
||||
|
||||
@@ -5,6 +5,7 @@ define([
|
||||
'./templating/templateSrv',
|
||||
'./dashboard/all',
|
||||
'./playlist/all',
|
||||
'./snapshot/all',
|
||||
'./panel/all',
|
||||
'./profile/profileCtrl',
|
||||
'./profile/changePasswordCtrl',
|
||||
|
||||
@@ -115,9 +115,9 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 120px">
|
||||
@@ -130,17 +130,6 @@
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 120px">
|
||||
Created at:
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 180px">
|
||||
{{formatDate(dashboardMeta.created)}}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 120px">
|
||||
Last updated by:
|
||||
@@ -150,6 +139,39 @@
|
||||
</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:
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 180px">
|
||||
{{formatDate(dashboardMeta.created)}}
|
||||
</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 by:
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 180px">
|
||||
{{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>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
public/app/features/snapshot/all.ts
Normal file
1
public/app/features/snapshot/all.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './snapshot_ctrl';
|
||||
39
public/app/features/snapshot/partials/snapshots.html
Normal file
39
public/app/features/snapshot/partials/snapshots.html
Normal file
@@ -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
public/app/features/snapshot/snapshot_ctrl.ts
Normal file
42
public/app/features/snapshot/snapshot_ctrl.ts
Normal file
@@ -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);
|
||||
@@ -90,18 +90,20 @@ function (angular, _, moment, dateMath) {
|
||||
return this.awsRequest({action: '__GetNamespaces'});
|
||||
};
|
||||
|
||||
this.getMetrics = function(namespace) {
|
||||
this.getMetrics = function(namespace, region) {
|
||||
return this.awsRequest({
|
||||
action: '__GetMetrics',
|
||||
region: region,
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionKeys = function(namespace) {
|
||||
this.getDimensionKeys = function(namespace, region) {
|
||||
return this.awsRequest({
|
||||
action: '__GetDimensions',
|
||||
region: region,
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace)
|
||||
}
|
||||
@@ -164,14 +166,14 @@ function (angular, _, moment, dateMath) {
|
||||
return this.getNamespaces();
|
||||
}
|
||||
|
||||
var metricNameQuery = query.match(/^metrics\(([^\)]+?)\)/);
|
||||
var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
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) {
|
||||
return this.getDimensionKeys(dimensionKeysQuery[1]);
|
||||
return this.getDimensionKeys(dimensionKeysQuery[1], dimensionKeysQuery[3]);
|
||||
}
|
||||
|
||||
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
|
||||
|
||||
@@ -102,7 +102,7 @@ function (angular, _) {
|
||||
var query = $q.when([]);
|
||||
|
||||
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') {
|
||||
var dimensionKey = $scope.dimSegments[$index-2].value;
|
||||
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
|
||||
@@ -160,7 +160,7 @@ function (angular, _) {
|
||||
};
|
||||
|
||||
$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));
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user