mirror of
https://github.com/grafana/grafana.git
synced 2025-09-27 07:43:42 +08:00
Added expire option to dashboard snapshots, #1623
This commit is contained in:
@ -1,10 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"strconv"
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
@ -17,46 +14,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
|
func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
|
||||||
|
cmd.Key = util.GetRandomString(32)
|
||||||
|
|
||||||
if cmd.External {
|
if cmd.External {
|
||||||
createExternalSnapshot(c, cmd)
|
cmd.OrgId = -1
|
||||||
return
|
metrics.M_Api_Dashboard_Snapshot_External.Inc(1)
|
||||||
|
} else {
|
||||||
|
cmd.OrgId = c.OrgId
|
||||||
|
metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Key = util.GetRandomString(32)
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
c.JsonApiErr(500, "Failed to create snaphost", err)
|
c.JsonApiErr(500, "Failed to create snaphost", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
|
|
||||||
|
|
||||||
c.JSON(200, util.DynMap{"key": cmd.Key, "url": setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)})
|
c.JSON(200, util.DynMap{"key": cmd.Key, "url": setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createExternalSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
|
|
||||||
metrics.M_Api_Dashboard_Snapshot_External.Inc(1)
|
|
||||||
|
|
||||||
cmd.External = false
|
|
||||||
json, _ := json.Marshal(cmd)
|
|
||||||
jsonData := bytes.NewBuffer(json)
|
|
||||||
|
|
||||||
client := http.Client{Timeout: time.Duration(5 * time.Second)}
|
|
||||||
resp, err := client.Post("http://snapshots-origin.raintank.io/api/snapshots", "application/json", jsonData)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.JsonApiErr(500, "Failed to publish external snapshot", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
|
||||||
c.WriteHeader(resp.StatusCode)
|
|
||||||
|
|
||||||
if resp.ContentLength > 0 {
|
|
||||||
bytes, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
c.Write(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDashboardSnapshot(c *middleware.Context) {
|
func GetDashboardSnapshot(c *middleware.Context) {
|
||||||
key := c.Params(":key")
|
key := c.Params(":key")
|
||||||
|
|
||||||
@ -68,14 +43,24 @@ func GetDashboardSnapshot(c *middleware.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
snapshot := query.Result
|
||||||
|
|
||||||
|
// expired snapshots should also be removed from db
|
||||||
|
if snapshot.Expires.Before(time.Now()) {
|
||||||
|
c.JsonApiErr(404, "Snapshot not found", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
dto := dtos.Dashboard{
|
dto := dtos.Dashboard{
|
||||||
Model: query.Result.Dashboard,
|
Model: snapshot.Dashboard,
|
||||||
Meta: dtos.DashboardMeta{IsSnapshot: true},
|
Meta: dtos.DashboardMeta{IsSnapshot: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.M_Api_Dashboard_Snapshot_Get.Inc(1)
|
metrics.M_Api_Dashboard_Snapshot_Get.Inc(1)
|
||||||
|
|
||||||
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
|
maxAge := int64(snapshot.Expires.Sub(time.Now()).Seconds())
|
||||||
|
|
||||||
|
c.Resp.Header().Set("Cache-Control", "public, max-age="+strconv.FormatInt(maxAge, 10))
|
||||||
|
|
||||||
c.JSON(200, dto)
|
c.JSON(200, dto)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ type DashboardSnapshot struct {
|
|||||||
Id int64
|
Id int64
|
||||||
Name string
|
Name string
|
||||||
Key string
|
Key string
|
||||||
|
OrgId int64
|
||||||
|
|
||||||
Expires time.Time
|
Expires time.Time
|
||||||
Created time.Time
|
Created time.Time
|
||||||
@ -20,8 +21,10 @@ type DashboardSnapshot struct {
|
|||||||
|
|
||||||
type CreateDashboardSnapshotCommand struct {
|
type CreateDashboardSnapshotCommand struct {
|
||||||
Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
|
Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
|
||||||
External bool
|
External bool `json:"external"`
|
||||||
|
Expires int64 `json:"expires"`
|
||||||
|
|
||||||
|
OrgId int64 `json:"-"`
|
||||||
Key string `json:"-"`
|
Key string `json:"-"`
|
||||||
|
|
||||||
Result *DashboardSnapshot
|
Result *DashboardSnapshot
|
||||||
|
@ -21,8 +21,8 @@ func TestApiKeyDataAccess(t *testing.T) {
|
|||||||
Convey("Should be able to get key by name", func() {
|
Convey("Should be able to get key by name", func() {
|
||||||
query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
||||||
err = GetApiKeyByName(&query)
|
err = GetApiKeyByName(&query)
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
So(query.Result, ShouldNotBeNil)
|
So(query.Result, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -16,10 +16,17 @@ func init() {
|
|||||||
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
|
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
|
||||||
return inTransaction(func(sess *xorm.Session) error {
|
return inTransaction(func(sess *xorm.Session) error {
|
||||||
|
|
||||||
|
// never
|
||||||
|
var expires = time.Now().Add(time.Hour * 24 * 365 * 50)
|
||||||
|
if cmd.Expires > 0 {
|
||||||
|
expires = time.Now().Add(time.Second * time.Duration(cmd.Expires))
|
||||||
|
}
|
||||||
|
|
||||||
snapshot := &m.DashboardSnapshot{
|
snapshot := &m.DashboardSnapshot{
|
||||||
Key: cmd.Key,
|
Key: cmd.Key,
|
||||||
|
OrgId: cmd.OrgId,
|
||||||
Dashboard: cmd.Dashboard,
|
Dashboard: cmd.Dashboard,
|
||||||
Expires: time.Unix(0, 0),
|
Expires: expires,
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
}
|
}
|
||||||
@ -32,8 +39,8 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
|
func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
|
||||||
var snapshot m.DashboardSnapshot
|
snapshot := m.DashboardSnapshot{Key: query.Key}
|
||||||
has, err := x.Where("key=?", query.Key).Get(&snapshot)
|
has, err := x.Get(&snapshot)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -21,4 +21,10 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
|
|||||||
|
|
||||||
mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(snapshotV4))
|
mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(snapshotV4))
|
||||||
addTableIndicesMigrations(mg, "v4", snapshotV4)
|
addTableIndicesMigrations(mg, "v4", snapshotV4)
|
||||||
|
|
||||||
|
mg.AddMigration("add org_id to dashboard_snapshot", new(AddColumnMigration).
|
||||||
|
Table("dashboard_snapshot").Column(&Column{Name: "org_id", Type: DB_BigInt, Nullable: true}))
|
||||||
|
|
||||||
|
mg.AddMigration("add index org_id to dashboard_snapshot",
|
||||||
|
NewAddIndexMigration(snapshotV4, &Index{Cols: []string{"org_id"}}))
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
x *xorm.Engine
|
x *xorm.Engine
|
||||||
dialect migrator.Dialect
|
dialect migrator.Dialect
|
||||||
tables []interface{}
|
|
||||||
|
|
||||||
HasEngine bool
|
HasEngine bool
|
||||||
|
|
||||||
@ -80,10 +79,6 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) {
|
|||||||
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
|
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := x.Sync2(tables...); err != nil {
|
|
||||||
return fmt.Errorf("sync database struct error: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enableLog {
|
if enableLog {
|
||||||
logPath := path.Join(setting.LogRootPath, "xorm.log")
|
logPath := path.Join(setting.LogRootPath, "xorm.log")
|
||||||
os.MkdirAll(path.Dir(logPath), os.ModePerm)
|
os.MkdirAll(path.Dir(logPath), os.ModePerm)
|
||||||
@ -94,12 +89,14 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) {
|
|||||||
}
|
}
|
||||||
x.Logger = xorm.NewSimpleLogger(f)
|
x.Logger = xorm.NewSimpleLogger(f)
|
||||||
|
|
||||||
x.ShowSQL = true
|
if setting.Env == setting.DEV {
|
||||||
x.ShowInfo = true
|
x.ShowSQL = false
|
||||||
x.ShowDebug = true
|
x.ShowInfo = false
|
||||||
|
x.ShowDebug = false
|
||||||
x.ShowErr = true
|
x.ShowErr = true
|
||||||
x.ShowWarn = true
|
x.ShowWarn = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -125,7 +122,7 @@ func getEngine() (*xorm.Engine, error) {
|
|||||||
DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
|
DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
|
||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
|
os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
|
||||||
cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc"
|
cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc&_loc=Local"
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
|
return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
|
||||||
}
|
}
|
||||||
|
1
pkg/services/sqlstore/sqlstore.goconvey
Normal file
1
pkg/services/sqlstore/sqlstore.goconvey
Normal file
@ -0,0 +1 @@
|
|||||||
|
-timeout=10s
|
@ -11,7 +11,7 @@ type TestDB struct {
|
|||||||
ConnStr string
|
ConnStr string
|
||||||
}
|
}
|
||||||
|
|
||||||
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"}
|
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"}
|
||||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8"}
|
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8"}
|
||||||
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
|
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
|
||||||
|
|
||||||
|
0
pkg/services/sqlstore/xorm.log
Normal file
0
pkg/services/sqlstore/xorm.log
Normal file
@ -79,6 +79,7 @@ function (angular, $, config) {
|
|||||||
meta.canEdit = false;
|
meta.canEdit = false;
|
||||||
meta.canSave = false;
|
meta.canSave = false;
|
||||||
meta.canStar = false;
|
meta.canStar = false;
|
||||||
|
meta.canShare = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.dashboardMeta = meta;
|
$scope.dashboardMeta = meta;
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
leaving only the visible metric data and series names embedded into your dashboard.
|
leaving only the visible metric data and series names embedded into your dashboard.
|
||||||
</p>
|
</p>
|
||||||
<p class="share-snapshot-info-text">
|
<p class="share-snapshot-info-text">
|
||||||
Keep in mind, your <strong>snapshot can be viewed any anyone</strong> that has the link and can reach the URL.
|
Keep in mind, your <strong>snapshot can be viewed by anyone</strong> that has the link and can reach the URL.
|
||||||
Share wisely.
|
Share wisely.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -79,11 +79,22 @@
|
|||||||
Expire
|
Expire
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<select class="input-small tight-form-input last" style="width: 211px" ng-model="snapshot.expire" ng-options="f.value as f.text for f in expireOptions"></select>
|
<select class="input-small tight-form-input last" style="width: 211px" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
|
||||||
</li>
|
</li>
|
||||||
</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: 110px"> -->
|
||||||
|
<!-- Access -->
|
||||||
|
<!-- </li> -->
|
||||||
|
<!-- <li> -->
|
||||||
|
<!-- <select class="input-small tight-form-input last" style="width: 211px" ng-model="snapshot.access" ng-options="f.value as f.text for f in accessOptions"></select> -->
|
||||||
|
<!-- </li> -->
|
||||||
|
<!-- </ul> -->
|
||||||
|
<!-- <div class="clearfix"></div> -->
|
||||||
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-if="step === 2" style="margin-top: 55px">
|
<div class="gf-form" ng-if="step === 2" style="margin-top: 55px">
|
||||||
|
@ -11,8 +11,7 @@ function (angular, _) {
|
|||||||
|
|
||||||
$scope.snapshot = {
|
$scope.snapshot = {
|
||||||
name: $scope.dashboard.title,
|
name: $scope.dashboard.title,
|
||||||
expire: 0,
|
expires: 0,
|
||||||
external: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.step = 1;
|
$scope.step = 1;
|
||||||
@ -24,6 +23,12 @@ function (angular, _) {
|
|||||||
{text: 'Never', value: 0},
|
{text: 'Never', value: 0},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$scope.accessOptions = [
|
||||||
|
{text: 'Anyone with the link', value: 1},
|
||||||
|
{text: 'Organization users', value: 2},
|
||||||
|
{text: 'Public on the web', value: 3},
|
||||||
|
];
|
||||||
|
|
||||||
$scope.createSnapshot = function(external) {
|
$scope.createSnapshot = function(external) {
|
||||||
$scope.dashboard.snapshot = {
|
$scope.dashboard.snapshot = {
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
@ -35,11 +40,11 @@ function (angular, _) {
|
|||||||
$rootScope.$broadcast('refresh');
|
$rootScope.$broadcast('refresh');
|
||||||
|
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
$scope.saveSnapshot();
|
$scope.saveSnapshot(external);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveSnapshot = function() {
|
$scope.saveSnapshot = function(external) {
|
||||||
var dash = angular.copy($scope.dashboard);
|
var dash = angular.copy($scope.dashboard);
|
||||||
// change title
|
// change title
|
||||||
dash.title = $scope.snapshot.name;
|
dash.title = $scope.snapshot.name;
|
||||||
@ -66,14 +71,19 @@ function (angular, _) {
|
|||||||
|
|
||||||
var cmdData = {
|
var cmdData = {
|
||||||
dashboard: dash,
|
dashboard: dash,
|
||||||
external: $scope.snapshot.external,
|
external: external === true,
|
||||||
expires: $scope.snapshot.expires,
|
expires: $scope.snapshot.expires,
|
||||||
};
|
};
|
||||||
|
|
||||||
backendSrv.post('/api/snapshots', cmdData).then(function(results) {
|
var apiUrl = '/api/snapshots/';
|
||||||
|
if (external) {
|
||||||
|
apiUrl = "http://snapshots-origin.raintank.io/api/snapshots";
|
||||||
|
}
|
||||||
|
|
||||||
|
backendSrv.post(apiUrl, cmdData).then(function(results) {
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
|
|
||||||
if ($scope.snapshot.external) {
|
if (external) {
|
||||||
$scope.snapshotUrl = results.url;
|
$scope.snapshotUrl = results.url;
|
||||||
} else {
|
} else {
|
||||||
var baseUrl = $location.absUrl().replace($location.url(), "");
|
var baseUrl = $location.absUrl().replace($location.url(), "");
|
||||||
|
Reference in New Issue
Block a user