mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Added expire option to dashboard snapshots, #1623
This commit is contained in:
parent
722d74a41b
commit
b1f85dc8f1
@ -1,10 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@ -17,46 +14,24 @@ import (
|
||||
)
|
||||
|
||||
func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapshotCommand) {
|
||||
cmd.Key = util.GetRandomString(32)
|
||||
|
||||
if cmd.External {
|
||||
createExternalSnapshot(c, cmd)
|
||||
return
|
||||
cmd.OrgId = -1
|
||||
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 {
|
||||
c.JsonApiErr(500, "Failed to create snaphost", err)
|
||||
return
|
||||
}
|
||||
|
||||
metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
|
||||
|
||||
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) {
|
||||
key := c.Params(":key")
|
||||
|
||||
@ -68,14 +43,24 @@ func GetDashboardSnapshot(c *middleware.Context) {
|
||||
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{
|
||||
Model: query.Result.Dashboard,
|
||||
Model: snapshot.Dashboard,
|
||||
Meta: dtos.DashboardMeta{IsSnapshot: true},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ import "time"
|
||||
|
||||
// DashboardSnapshot model
|
||||
type DashboardSnapshot struct {
|
||||
Id int64
|
||||
Name string
|
||||
Key string
|
||||
Id int64
|
||||
Name string
|
||||
Key string
|
||||
OrgId int64
|
||||
|
||||
Expires time.Time
|
||||
Created time.Time
|
||||
@ -20,9 +21,11 @@ type DashboardSnapshot struct {
|
||||
|
||||
type CreateDashboardSnapshotCommand struct {
|
||||
Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
|
||||
External bool
|
||||
External bool `json:"external"`
|
||||
Expires int64 `json:"expires"`
|
||||
|
||||
Key string `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
Key string `json:"-"`
|
||||
|
||||
Result *DashboardSnapshot
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ func TestApiKeyDataAccess(t *testing.T) {
|
||||
Convey("Should be able to get key by name", func() {
|
||||
query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
|
||||
err = GetApiKeyByName(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
|
@ -16,10 +16,17 @@ func init() {
|
||||
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) 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{
|
||||
Key: cmd.Key,
|
||||
OrgId: cmd.OrgId,
|
||||
Dashboard: cmd.Dashboard,
|
||||
Expires: time.Unix(0, 0),
|
||||
Expires: expires,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
@ -32,8 +39,8 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
|
||||
}
|
||||
|
||||
func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
|
||||
var snapshot m.DashboardSnapshot
|
||||
has, err := x.Where("key=?", query.Key).Get(&snapshot)
|
||||
snapshot := m.DashboardSnapshot{Key: query.Key}
|
||||
has, err := x.Get(&snapshot)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -21,4 +21,10 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
|
||||
|
||||
mg.AddMigration("create dashboard_snapshot table v4", NewAddTableMigration(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 (
|
||||
x *xorm.Engine
|
||||
dialect migrator.Dialect
|
||||
tables []interface{}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if err := x.Sync2(tables...); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %v\n", err)
|
||||
}
|
||||
|
||||
if enableLog {
|
||||
logPath := path.Join(setting.LogRootPath, "xorm.log")
|
||||
os.MkdirAll(path.Dir(logPath), os.ModePerm)
|
||||
@ -94,11 +89,13 @@ func SetEngine(engine *xorm.Engine, enableLog bool) (err error) {
|
||||
}
|
||||
x.Logger = xorm.NewSimpleLogger(f)
|
||||
|
||||
x.ShowSQL = true
|
||||
x.ShowInfo = true
|
||||
x.ShowDebug = true
|
||||
x.ShowErr = true
|
||||
x.ShowWarn = true
|
||||
if setting.Env == setting.DEV {
|
||||
x.ShowSQL = false
|
||||
x.ShowInfo = false
|
||||
x.ShowDebug = false
|
||||
x.ShowErr = true
|
||||
x.ShowWarn = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -125,7 +122,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode)
|
||||
case "sqlite3":
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
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_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.canSave = false;
|
||||
meta.canStar = false;
|
||||
meta.canShare = false;
|
||||
}
|
||||
|
||||
$scope.dashboardMeta = meta;
|
||||
|
@ -55,7 +55,7 @@
|
||||
leaving only the visible metric data and series names embedded into your dashboard.
|
||||
</p>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@ -79,11 +79,22 @@
|
||||
Expire
|
||||
</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>
|
||||
</ul>
|
||||
<div class="clearfix"></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 class="gf-form" ng-if="step === 2" style="margin-top: 55px">
|
||||
|
@ -11,8 +11,7 @@ function (angular, _) {
|
||||
|
||||
$scope.snapshot = {
|
||||
name: $scope.dashboard.title,
|
||||
expire: 0,
|
||||
external: false,
|
||||
expires: 0,
|
||||
};
|
||||
|
||||
$scope.step = 1;
|
||||
@ -24,6 +23,12 @@ function (angular, _) {
|
||||
{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.dashboard.snapshot = {
|
||||
timestamp: new Date()
|
||||
@ -35,11 +40,11 @@ function (angular, _) {
|
||||
$rootScope.$broadcast('refresh');
|
||||
|
||||
$timeout(function() {
|
||||
$scope.saveSnapshot();
|
||||
$scope.saveSnapshot(external);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
$scope.saveSnapshot = function() {
|
||||
$scope.saveSnapshot = function(external) {
|
||||
var dash = angular.copy($scope.dashboard);
|
||||
// change title
|
||||
dash.title = $scope.snapshot.name;
|
||||
@ -66,14 +71,19 @@ function (angular, _) {
|
||||
|
||||
var cmdData = {
|
||||
dashboard: dash,
|
||||
external: $scope.snapshot.external,
|
||||
external: external === true,
|
||||
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;
|
||||
|
||||
if ($scope.snapshot.external) {
|
||||
if (external) {
|
||||
$scope.snapshotUrl = results.url;
|
||||
} else {
|
||||
var baseUrl = $location.absUrl().replace($location.url(), "");
|
||||
|
Loading…
Reference in New Issue
Block a user