Commit b1f85dc8 by Torkel Ödegaard

Added expire option to dashboard snapshots, #1623

parent 722d74a4
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)
}
......
......@@ -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"}
......
......@@ -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&#45;form"> -->
<!-- <ul class="tight&#45;form&#45;list"> -->
<!-- <li class="tight&#45;form&#45;item" style="width: 110px"> -->
<!-- Access -->
<!-- </li> -->
<!-- <li> -->
<!-- <select class="input&#45;small tight&#45;form&#45;input last" style="width: 211px" ng&#45;model="snapshot.access" ng&#45;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(), "");
......
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