diff --git a/pkg/api/api.go b/pkg/api/api.go
index eb17c65ac73..c3a3728338d 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -223,6 +223,13 @@ func (hs *HttpServer) registerRoutes() {
// Dashboard
r.Group("/dashboards", func() {
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
+
+ r.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
+ r.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
+ r.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
+
+ r.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
+
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", wrap(GetHomeDashboard))
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 55925c4faf6..df0cbbd745c 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -2,12 +2,14 @@ package api
import (
"encoding/json"
+ "fmt"
"os"
"path"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/dashdiffs"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
@@ -60,6 +62,9 @@ func GetDashboard(c *middleware.Context) {
creator = getUserLogin(dash.CreatedBy)
}
+ // make sure db version is in sync with json model version
+ dash.Data.Set("version", dash.Version)
+
dto := dtos.DashboardFullWithMeta{
Dashboard: dash.Data,
Meta: dtos.DashboardMeta{
@@ -77,6 +82,7 @@ func GetDashboard(c *middleware.Context) {
},
}
+ // TODO(ben): copy this performance metrics logic for the new API endpoints added
c.TimeRequest(metrics.M_Api_Dashboard_Get)
c.JSON(200, dto)
}
@@ -114,18 +120,15 @@ func DeleteDashboard(c *middleware.Context) {
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
cmd.OrgId = c.OrgId
-
- if !c.IsSignedIn {
- cmd.UserId = -1
- } else {
- cmd.UserId = c.UserId
- }
+ cmd.UserId = c.UserId
dash := cmd.GetDashboardModel()
+
// Check if Title is empty
if dash.Title == "" {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
}
+
if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil {
@@ -255,6 +258,135 @@ func GetDashboardFromJsonFile(c *middleware.Context) {
c.JSON(200, &dash)
}
+// GetDashboardVersions returns all dashboard versions as JSON
+func GetDashboardVersions(c *middleware.Context) Response {
+ dashboardId := c.ParamsInt64(":dashboardId")
+ limit := c.QueryInt("limit")
+ start := c.QueryInt("start")
+
+ if limit == 0 {
+ limit = 1000
+ }
+
+ query := m.GetDashboardVersionsQuery{
+ OrgId: c.OrgId,
+ DashboardId: dashboardId,
+ Limit: limit,
+ Start: start,
+ }
+
+ if err := bus.Dispatch(&query); err != nil {
+ return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err)
+ }
+
+ for _, version := range query.Result {
+ if version.RestoredFrom == version.Version {
+ version.Message = "Initial save (created by migration)"
+ continue
+ }
+
+ if version.RestoredFrom > 0 {
+ version.Message = fmt.Sprintf("Restored from version %d", version.RestoredFrom)
+ continue
+ }
+
+ if version.ParentVersion == 0 {
+ version.Message = "Initial save"
+ }
+ }
+
+ return Json(200, query.Result)
+}
+
+// GetDashboardVersion returns the dashboard version with the given ID.
+func GetDashboardVersion(c *middleware.Context) Response {
+ dashboardId := c.ParamsInt64(":dashboardId")
+ version := c.ParamsInt(":id")
+
+ query := m.GetDashboardVersionQuery{
+ OrgId: c.OrgId,
+ DashboardId: dashboardId,
+ Version: version,
+ }
+
+ if err := bus.Dispatch(&query); err != nil {
+ return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err)
+ }
+
+ creator := "Anonymous"
+ if query.Result.CreatedBy > 0 {
+ creator = getUserLogin(query.Result.CreatedBy)
+ }
+
+ dashVersionMeta := &m.DashboardVersionMeta{
+ DashboardVersion: *query.Result,
+ CreatedBy: creator,
+ }
+
+ return Json(200, dashVersionMeta)
+}
+
+// POST /api/dashboards/calculate-diff performs diffs on two dashboards
+func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiffOptions) Response {
+
+ options := dashdiffs.Options{
+ OrgId: c.OrgId,
+ DiffType: dashdiffs.ParseDiffType(apiOptions.DiffType),
+ Base: dashdiffs.DiffTarget{
+ DashboardId: apiOptions.Base.DashboardId,
+ Version: apiOptions.Base.Version,
+ UnsavedDashboard: apiOptions.Base.UnsavedDashboard,
+ },
+ New: dashdiffs.DiffTarget{
+ DashboardId: apiOptions.New.DashboardId,
+ Version: apiOptions.New.Version,
+ UnsavedDashboard: apiOptions.New.UnsavedDashboard,
+ },
+ }
+
+ result, err := dashdiffs.CalculateDiff(&options)
+ if err != nil {
+ if err == m.ErrDashboardVersionNotFound {
+ return ApiError(404, "Dashboard version not found", err)
+ }
+ return ApiError(500, "Unable to compute diff", err)
+ }
+
+ if options.DiffType == dashdiffs.DiffDelta {
+ return Respond(200, result.Delta).Header("Content-Type", "application/json")
+ } else {
+ return Respond(200, result.Delta).Header("Content-Type", "text/html")
+ }
+}
+
+// RestoreDashboardVersion restores a dashboard to the given version.
+func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
+ dashboardId := c.ParamsInt64(":dashboardId")
+
+ dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId}
+ if err := bus.Dispatch(&dashQuery); err != nil {
+ return ApiError(404, "Dashboard not found", nil)
+ }
+
+ versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId}
+ if err := bus.Dispatch(&versionQuery); err != nil {
+ return ApiError(404, "Dashboard version not found", nil)
+ }
+
+ dashboard := dashQuery.Result
+ version := versionQuery.Result
+
+ saveCmd := m.SaveDashboardCommand{}
+ saveCmd.RestoredFrom = version.Version
+ saveCmd.OrgId = c.OrgId
+ saveCmd.UserId = c.UserId
+ saveCmd.Dashboard = version.Data
+ saveCmd.Dashboard.Set("version", dashboard.Version)
+ saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
+
+ return PostDashboard(c, saveCmd)
+}
+
func GetDashboardTags(c *middleware.Context) {
query := m.GetDashboardTagsQuery{OrgId: c.OrgId}
err := bus.Dispatch(&query)
diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go
new file mode 100644
index 00000000000..9ef9a96edc4
--- /dev/null
+++ b/pkg/api/dtos/dashboard.go
@@ -0,0 +1,49 @@
+package dtos
+
+import (
+ "time"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+)
+
+type DashboardMeta struct {
+ IsStarred bool `json:"isStarred,omitempty"`
+ IsHome bool `json:"isHome,omitempty"`
+ IsSnapshot bool `json:"isSnapshot,omitempty"`
+ Type string `json:"type,omitempty"`
+ CanSave bool `json:"canSave"`
+ CanEdit bool `json:"canEdit"`
+ CanStar bool `json:"canStar"`
+ Slug string `json:"slug"`
+ Expires time.Time `json:"expires"`
+ 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 {
+ Meta DashboardMeta `json:"meta"`
+ Dashboard *simplejson.Json `json:"dashboard"`
+}
+
+type DashboardRedirect struct {
+ RedirectUri string `json:"redirectUri"`
+}
+
+type CalculateDiffOptions struct {
+ Base CalculateDiffTarget `json:"base" binding:"Required"`
+ New CalculateDiffTarget `json:"new" binding:"Required"`
+ DiffType string `json:"diffType" binding:"Required"`
+}
+
+type CalculateDiffTarget struct {
+ DashboardId int64 `json:"dashboardId"`
+ Version int `json:"version"`
+ UnsavedDashboard *simplejson.Json `json:"unsavedDashboard"`
+}
+
+type RestoreDashboardVersionCommand struct {
+ Version int `json:"version" binding:"Required"`
+}
diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go
index 2d8bdcbae03..d1c346e9539 100644
--- a/pkg/api/dtos/models.go
+++ b/pkg/api/dtos/models.go
@@ -4,7 +4,6 @@ import (
"crypto/md5"
"fmt"
"strings"
- "time"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
@@ -38,32 +37,6 @@ type CurrentUser struct {
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
}
-type DashboardMeta struct {
- IsStarred bool `json:"isStarred,omitempty"`
- IsHome bool `json:"isHome,omitempty"`
- IsSnapshot bool `json:"isSnapshot,omitempty"`
- Type string `json:"type,omitempty"`
- CanSave bool `json:"canSave"`
- CanEdit bool `json:"canEdit"`
- CanStar bool `json:"canStar"`
- Slug string `json:"slug"`
- Expires time.Time `json:"expires"`
- 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 {
- Meta DashboardMeta `json:"meta"`
- Dashboard *simplejson.Json `json:"dashboard"`
-}
-
-type DashboardRedirect struct {
- RedirectUri string `json:"redirectUri"`
-}
-
type DataSource struct {
Id int64 `json:"id"`
OrgId int64 `json:"orgId"`
diff --git a/pkg/components/dashdiffs/compare.go b/pkg/components/dashdiffs/compare.go
new file mode 100644
index 00000000000..f5f2104cb92
--- /dev/null
+++ b/pkg/components/dashdiffs/compare.go
@@ -0,0 +1,149 @@
+package dashdiffs
+
+import (
+ "encoding/json"
+ "errors"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
+ diff "github.com/yudai/gojsondiff"
+ deltaFormatter "github.com/yudai/gojsondiff/formatter"
+)
+
+var (
+ // ErrUnsupportedDiffType occurs when an invalid diff type is used.
+ ErrUnsupportedDiffType = errors.New("dashdiff: unsupported diff type")
+
+ // ErrNilDiff occurs when two compared interfaces are identical.
+ ErrNilDiff = errors.New("dashdiff: diff is nil")
+
+ diffLogger = log.New("dashdiffs")
+)
+
+type DiffType int
+
+const (
+ DiffJSON DiffType = iota
+ DiffBasic
+ DiffDelta
+)
+
+type Options struct {
+ OrgId int64
+ Base DiffTarget
+ New DiffTarget
+ DiffType DiffType
+}
+
+type DiffTarget struct {
+ DashboardId int64
+ Version int
+ UnsavedDashboard *simplejson.Json
+}
+
+type Result struct {
+ Delta []byte `json:"delta"`
+}
+
+func ParseDiffType(diff string) DiffType {
+ switch diff {
+ case "json":
+ return DiffJSON
+ case "basic":
+ return DiffBasic
+ case "delta":
+ return DiffDelta
+ }
+ return DiffBasic
+}
+
+// CompareDashboardVersionsCommand computes the JSON diff of two versions,
+// assigning the delta of the diff to the `Delta` field.
+func CalculateDiff(options *Options) (*Result, error) {
+ baseVersionQuery := models.GetDashboardVersionQuery{
+ DashboardId: options.Base.DashboardId,
+ Version: options.Base.Version,
+ OrgId: options.OrgId,
+ }
+
+ if err := bus.Dispatch(&baseVersionQuery); err != nil {
+ return nil, err
+ }
+
+ newVersionQuery := models.GetDashboardVersionQuery{
+ DashboardId: options.New.DashboardId,
+ Version: options.New.Version,
+ OrgId: options.OrgId,
+ }
+
+ if err := bus.Dispatch(&newVersionQuery); err != nil {
+ return nil, err
+ }
+
+ baseData := baseVersionQuery.Result.Data
+ newData := newVersionQuery.Result.Data
+
+ left, jsonDiff, err := getDiff(baseData, newData)
+ if err != nil {
+ return nil, err
+ }
+
+ result := &Result{}
+
+ switch options.DiffType {
+ case DiffDelta:
+
+ deltaOutput, err := deltaFormatter.NewDeltaFormatter().Format(jsonDiff)
+ if err != nil {
+ return nil, err
+ }
+ result.Delta = []byte(deltaOutput)
+
+ case DiffJSON:
+ jsonOutput, err := NewJSONFormatter(left).Format(jsonDiff)
+ if err != nil {
+ return nil, err
+ }
+ result.Delta = []byte(jsonOutput)
+
+ case DiffBasic:
+ basicOutput, err := NewBasicFormatter(left).Format(jsonDiff)
+ if err != nil {
+ return nil, err
+ }
+ result.Delta = basicOutput
+
+ default:
+ return nil, ErrUnsupportedDiffType
+ }
+
+ return result, nil
+}
+
+// getDiff computes the diff of two dashboard versions.
+func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error) {
+ leftBytes, err := baseData.Encode()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ rightBytes, err := newData.Encode()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ jsonDiff, err := diff.New().Compare(leftBytes, rightBytes)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if !jsonDiff.Modified() {
+ return nil, nil, ErrNilDiff
+ }
+
+ left := make(map[string]interface{})
+ err = json.Unmarshal(leftBytes, &left)
+ return left, jsonDiff, nil
+}
diff --git a/pkg/components/dashdiffs/formatter_basic.go b/pkg/components/dashdiffs/formatter_basic.go
new file mode 100644
index 00000000000..01c7757112d
--- /dev/null
+++ b/pkg/components/dashdiffs/formatter_basic.go
@@ -0,0 +1,339 @@
+package dashdiffs
+
+import (
+ "bytes"
+ "html/template"
+
+ diff "github.com/yudai/gojsondiff"
+)
+
+// A BasicDiff holds the stateful values that are used when generating a basic
+// diff from JSON tokens.
+type BasicDiff struct {
+ narrow string
+ keysIdent int
+ writing bool
+ LastIndent int
+ Block *BasicBlock
+ Change *BasicChange
+ Summary *BasicSummary
+}
+
+// A BasicBlock represents a top-level element in a basic diff.
+type BasicBlock struct {
+ Title string
+ Old interface{}
+ New interface{}
+ Change ChangeType
+ Changes []*BasicChange
+ Summaries []*BasicSummary
+ LineStart int
+ LineEnd int
+}
+
+// A BasicChange represents the change from an old to new value. There are many
+// BasicChanges in a BasicBlock.
+type BasicChange struct {
+ Key string
+ Old interface{}
+ New interface{}
+ Change ChangeType
+ LineStart int
+ LineEnd int
+}
+
+// A BasicSummary represents the changes within a basic block that're too deep
+// or verbose to be represented in the top-level BasicBlock element, or in the
+// BasicChange. Instead of showing the values in this case, we simply print
+// the key and count how many times the given change was applied to that
+// element.
+type BasicSummary struct {
+ Key string
+ Change ChangeType
+ Count int
+ LineStart int
+ LineEnd int
+}
+
+type BasicFormatter struct {
+ jsonDiff *JSONFormatter
+ tpl *template.Template
+}
+
+func NewBasicFormatter(left interface{}) *BasicFormatter {
+ tpl := template.Must(template.New("block").Funcs(tplFuncMap).Parse(tplBlock))
+ tpl = template.Must(tpl.New("change").Funcs(tplFuncMap).Parse(tplChange))
+ tpl = template.Must(tpl.New("summary").Funcs(tplFuncMap).Parse(tplSummary))
+
+ return &BasicFormatter{
+ jsonDiff: NewJSONFormatter(left),
+ tpl: tpl,
+ }
+}
+
+func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
+ // calling jsonDiff.Format(d) populates the JSON diff's "Lines" value,
+ // which we use to compute the basic dif
+ _, err := b.jsonDiff.Format(d)
+ if err != nil {
+ return nil, err
+ }
+
+ bd := &BasicDiff{}
+ blocks := bd.Basic(b.jsonDiff.Lines)
+ buf := &bytes.Buffer{}
+
+ err = b.tpl.ExecuteTemplate(buf, "block", blocks)
+ if err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// Basic is V2 of the basic diff
+func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
+ // init an array you can append to for the basic "blocks"
+ blocks := make([]*BasicBlock, 0)
+
+ // iterate through each line
+ for _, line := range lines {
+ // TODO: this condition needs an explaination? what does it mean?
+ if b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil {
+ if b.Block != nil {
+ blocks = append(blocks, b.Block)
+ }
+ }
+
+ b.LastIndent = line.Indent
+
+ // TODO: why special handling for indent 2?
+ if line.Indent == 1 {
+ switch line.Change {
+ case ChangeNil:
+ if line.Change == ChangeNil {
+ if line.Key != "" {
+ b.Block = &BasicBlock{
+ Title: line.Key,
+ Change: line.Change,
+ }
+ }
+ }
+
+ case ChangeAdded, ChangeDeleted:
+ blocks = append(blocks, &BasicBlock{
+ Title: line.Key,
+ Change: line.Change,
+ New: line.Val,
+ LineStart: line.LineNum,
+ })
+
+ case ChangeOld:
+ b.Block = &BasicBlock{
+ Title: line.Key,
+ Old: line.Val,
+ Change: line.Change,
+ LineStart: line.LineNum,
+ }
+
+ case ChangeNew:
+ b.Block.New = line.Val
+ b.Block.LineEnd = line.LineNum
+
+ // then write out the change
+ blocks = append(blocks, b.Block)
+ default:
+ // ok
+ }
+ }
+
+ // TODO: why special handling for indent > 2 ?
+ // Other Lines
+ if line.Indent > 1 {
+ // Ensure single line change
+ if line.Key != "" && line.Val != nil && !b.writing {
+ switch line.Change {
+ case ChangeAdded, ChangeDeleted:
+
+ b.Block.Changes = append(b.Block.Changes, &BasicChange{
+ Key: line.Key,
+ Change: line.Change,
+ New: line.Val,
+ LineStart: line.LineNum,
+ })
+
+ case ChangeOld:
+ b.Change = &BasicChange{
+ Key: line.Key,
+ Change: line.Change,
+ Old: line.Val,
+ LineStart: line.LineNum,
+ }
+
+ case ChangeNew:
+ b.Change.New = line.Val
+ b.Change.LineEnd = line.LineNum
+ b.Block.Changes = append(b.Block.Changes, b.Change)
+
+ default:
+ //ok
+ }
+
+ } else {
+ if line.Change != ChangeUnchanged {
+ if line.Key != "" {
+ b.narrow = line.Key
+ b.keysIdent = line.Indent
+ }
+
+ if line.Change != ChangeNil {
+ if !b.writing {
+ b.writing = true
+ key := b.Block.Title
+
+ if b.narrow != "" {
+ key = b.narrow
+ if b.keysIdent > line.Indent {
+ key = b.Block.Title
+ }
+ }
+
+ b.Summary = &BasicSummary{
+ Key: key,
+ Change: line.Change,
+ LineStart: line.LineNum,
+ }
+ }
+ }
+ } else {
+ if b.writing {
+ b.writing = false
+ b.Summary.LineEnd = line.LineNum
+ b.Block.Summaries = append(b.Block.Summaries, b.Summary)
+ }
+ }
+ }
+ }
+ }
+
+ return blocks
+}
+
+// encStateMap is used in the template helper
+var (
+ encStateMap = map[ChangeType]string{
+ ChangeAdded: "added",
+ ChangeDeleted: "deleted",
+ ChangeOld: "changed",
+ ChangeNew: "changed",
+ }
+
+ // tplFuncMap is the function map for each template
+ tplFuncMap = template.FuncMap{
+ "getChange": func(c ChangeType) string {
+ state, ok := encStateMap[c]
+ if !ok {
+ return "changed"
+ }
+ return state
+ },
+ }
+)
+
+var (
+ // tplBlock is the whole thing
+ tplBlock = `{{ define "block" -}}
+{{ range . }}
+
+
+
+
+ {{ .Title }} {{ getChange .Change }}
+
+
+
+
+ {{ if .Old }}
+
{{ .Old }}
+
+ {{ end }}
+ {{ if .New }}
+
{{ .New }}
+ {{ end }}
+
+ {{ if .LineStart }}
+
+ {{ end }}
+
+
+
+
+ {{ range .Changes }}
+
+ {{ template "change" . }}
+
+ {{ end }}
+
+
+ {{ range .Summaries }}
+ {{ template "summary" . }}
+ {{ end }}
+
+
+{{ end }}
+{{ end }}`
+
+ // tplChange is the template for changes
+ tplChange = `{{ define "change" -}}
+
+
+ {{ getChange .Change }} {{ .Key }}
+
+
+ {{ if .Old }}
+
{{ .Old }}
+
+ {{ end }}
+ {{ if .New }}
+
{{ .New }}
+ {{ end }}
+
+
+ {{ if .LineStart }}
+
+ {{ end }}
+
+
+{{ end }}`
+
+ // tplSummary is for basis summaries
+ tplSummary = `{{ define "summary" -}}
+
+
+
+ {{ if .Count }}
+ {{ .Count }}
+ {{ end }}
+
+ {{ if .Key }}
+ {{ .Key }}
+ {{ getChange .Change }}
+ {{ end }}
+
+ {{ if .LineStart }}
+
+ {{ end }}
+
+{{ end }}`
+)
diff --git a/pkg/components/dashdiffs/formatter_json.go b/pkg/components/dashdiffs/formatter_json.go
new file mode 100644
index 00000000000..a2807b15992
--- /dev/null
+++ b/pkg/components/dashdiffs/formatter_json.go
@@ -0,0 +1,477 @@
+package dashdiffs
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "html/template"
+ "sort"
+
+ diff "github.com/yudai/gojsondiff"
+)
+
+type ChangeType int
+
+const (
+ ChangeNil ChangeType = iota
+ ChangeAdded
+ ChangeDeleted
+ ChangeOld
+ ChangeNew
+ ChangeUnchanged
+)
+
+var (
+ // changeTypeToSymbol is used for populating the terminating characer in
+ // the diff
+ changeTypeToSymbol = map[ChangeType]string{
+ ChangeNil: "",
+ ChangeAdded: "+",
+ ChangeDeleted: "-",
+ ChangeOld: "-",
+ ChangeNew: "+",
+ }
+
+ // changeTypeToName is used for populating class names in the diff
+ changeTypeToName = map[ChangeType]string{
+ ChangeNil: "same",
+ ChangeAdded: "added",
+ ChangeDeleted: "deleted",
+ ChangeOld: "old",
+ ChangeNew: "new",
+ }
+)
+
+var (
+ // tplJSONDiffWrapper is the template that wraps a diff
+ tplJSONDiffWrapper = `{{ define "JSONDiffWrapper" -}}
+ {{ range $index, $element := . }}
+ {{ template "JSONDiffLine" $element }}
+ {{ end }}
+{{ end }}`
+
+ // tplJSONDiffLine is the template that prints each line in a diff
+ tplJSONDiffLine = `{{ define "JSONDiffLine" -}}
+
+
+ {{if .LeftLine }}{{ .LeftLine }}{{ end }}
+
+
+ {{if .RightLine }}{{ .RightLine }}{{ end }}
+
+
+ {{ .Text }}
+
+ {{ ctos .Change }}
+
+{{ end }}`
+)
+
+var diffTplFuncs = template.FuncMap{
+ "ctos": func(c ChangeType) string {
+ if symbol, ok := changeTypeToSymbol[c]; ok {
+ return symbol
+ }
+ return ""
+ },
+ "cton": func(c ChangeType) string {
+ if name, ok := changeTypeToName[c]; ok {
+ return name
+ }
+ return ""
+ },
+}
+
+// JSONLine contains the data required to render each line of the JSON diff
+// and contains the data required to produce the tokens output in the basic
+// diff.
+type JSONLine struct {
+ LineNum int `json:"line"`
+ LeftLine int `json:"leftLine"`
+ RightLine int `json:"rightLine"`
+ Indent int `json:"indent"`
+ Text string `json:"text"`
+ Change ChangeType `json:"changeType"`
+ Key string `json:"key"`
+ Val interface{} `json:"value"`
+}
+
+func NewJSONFormatter(left interface{}) *JSONFormatter {
+ tpl := template.Must(template.New("JSONDiffWrapper").Funcs(diffTplFuncs).Parse(tplJSONDiffWrapper))
+ tpl = template.Must(tpl.New("JSONDiffLine").Funcs(diffTplFuncs).Parse(tplJSONDiffLine))
+
+ return &JSONFormatter{
+ left: left,
+ Lines: []*JSONLine{},
+ tpl: tpl,
+ path: []string{},
+ size: []int{},
+ lineCount: 0,
+ inArray: []bool{},
+ }
+}
+
+type JSONFormatter struct {
+ left interface{}
+ path []string
+ size []int
+ inArray []bool
+ lineCount int
+ leftLine int
+ rightLine int
+ line *AsciiLine
+ Lines []*JSONLine
+ tpl *template.Template
+}
+
+type AsciiLine struct {
+ // the type of change
+ change ChangeType
+
+ // the actual changes - no formatting
+ key string
+ val interface{}
+
+ // level of indentation for the current line
+ indent int
+
+ // buffer containing the fully formatted line
+ buffer *bytes.Buffer
+}
+
+func (f *JSONFormatter) Format(diff diff.Diff) (result string, err error) {
+ if v, ok := f.left.(map[string]interface{}); ok {
+ f.formatObject(v, diff)
+ } else if v, ok := f.left.([]interface{}); ok {
+ f.formatArray(v, diff)
+ } else {
+ return "", fmt.Errorf("expected map[string]interface{} or []interface{}, got %T",
+ f.left)
+ }
+
+ b := &bytes.Buffer{}
+ err = f.tpl.ExecuteTemplate(b, "JSONDiffWrapper", f.Lines)
+ if err != nil {
+ fmt.Printf("%v\n", err)
+ return "", err
+ }
+ return b.String(), nil
+}
+
+func (f *JSONFormatter) formatObject(left map[string]interface{}, df diff.Diff) {
+ f.addLineWith(ChangeNil, "{")
+ f.push("ROOT", len(left), false)
+ f.processObject(left, df.Deltas())
+ f.pop()
+ f.addLineWith(ChangeNil, "}")
+}
+
+func (f *JSONFormatter) formatArray(left []interface{}, df diff.Diff) {
+ f.addLineWith(ChangeNil, "[")
+ f.push("ROOT", len(left), true)
+ f.processArray(left, df.Deltas())
+ f.pop()
+ f.addLineWith(ChangeNil, "]")
+}
+
+func (f *JSONFormatter) processArray(array []interface{}, deltas []diff.Delta) error {
+ patchedIndex := 0
+ for index, value := range array {
+ f.processItem(value, deltas, diff.Index(index))
+ patchedIndex++
+ }
+
+ // additional Added
+ for _, delta := range deltas {
+ switch delta.(type) {
+ case *diff.Added:
+ d := delta.(*diff.Added)
+ // skip items already processed
+ if int(d.Position.(diff.Index)) < len(array) {
+ continue
+ }
+ f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
+ }
+ }
+
+ return nil
+}
+
+func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []diff.Delta) error {
+ names := sortKeys(object)
+ for _, name := range names {
+ value := object[name]
+ f.processItem(value, deltas, diff.Name(name))
+ }
+
+ // Added
+ for _, delta := range deltas {
+ switch delta.(type) {
+ case *diff.Added:
+ d := delta.(*diff.Added)
+ f.printRecursive(d.Position.String(), d.Value, ChangeAdded)
+ }
+ }
+
+ return nil
+}
+
+func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, position diff.Position) error {
+ matchedDeltas := f.searchDeltas(deltas, position)
+ positionStr := position.String()
+ if len(matchedDeltas) > 0 {
+ for _, matchedDelta := range matchedDeltas {
+
+ switch matchedDelta.(type) {
+ case *diff.Object:
+ d := matchedDelta.(*diff.Object)
+ switch value.(type) {
+ case map[string]interface{}:
+ //ok
+ default:
+ return errors.New("Type mismatch")
+ }
+ o := value.(map[string]interface{})
+
+ f.newLine(ChangeNil)
+ f.printKey(positionStr)
+ f.print("{")
+ f.closeLine()
+ f.push(positionStr, len(o), false)
+ f.processObject(o, d.Deltas)
+ f.pop()
+ f.newLine(ChangeNil)
+ f.print("}")
+ f.printComma()
+ f.closeLine()
+
+ case *diff.Array:
+ d := matchedDelta.(*diff.Array)
+ switch value.(type) {
+ case []interface{}:
+ //ok
+ default:
+ return errors.New("Type mismatch")
+ }
+ a := value.([]interface{})
+
+ f.newLine(ChangeNil)
+ f.printKey(positionStr)
+ f.print("[")
+ f.closeLine()
+ f.push(positionStr, len(a), true)
+ f.processArray(a, d.Deltas)
+ f.pop()
+ f.newLine(ChangeNil)
+ f.print("]")
+ f.printComma()
+ f.closeLine()
+
+ case *diff.Added:
+ d := matchedDelta.(*diff.Added)
+ f.printRecursive(positionStr, d.Value, ChangeAdded)
+ f.size[len(f.size)-1]++
+
+ case *diff.Modified:
+ d := matchedDelta.(*diff.Modified)
+ savedSize := f.size[len(f.size)-1]
+ f.printRecursive(positionStr, d.OldValue, ChangeOld)
+ f.size[len(f.size)-1] = savedSize
+ f.printRecursive(positionStr, d.NewValue, ChangeNew)
+
+ case *diff.TextDiff:
+ savedSize := f.size[len(f.size)-1]
+ d := matchedDelta.(*diff.TextDiff)
+ f.printRecursive(positionStr, d.OldValue, ChangeOld)
+ f.size[len(f.size)-1] = savedSize
+ f.printRecursive(positionStr, d.NewValue, ChangeNew)
+
+ case *diff.Deleted:
+ d := matchedDelta.(*diff.Deleted)
+ f.printRecursive(positionStr, d.Value, ChangeDeleted)
+
+ default:
+ return errors.New("Unknown Delta type detected")
+ }
+
+ }
+ } else {
+ f.printRecursive(positionStr, value, ChangeUnchanged)
+ }
+
+ return nil
+}
+
+func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, postion diff.Position) (results []diff.Delta) {
+ results = make([]diff.Delta, 0)
+ for _, delta := range deltas {
+ switch delta.(type) {
+ case diff.PostDelta:
+ if delta.(diff.PostDelta).PostPosition() == postion {
+ results = append(results, delta)
+ }
+ case diff.PreDelta:
+ if delta.(diff.PreDelta).PrePosition() == postion {
+ results = append(results, delta)
+ }
+ default:
+ panic("heh")
+ }
+ }
+ return
+}
+
+func (f *JSONFormatter) push(name string, size int, array bool) {
+ f.path = append(f.path, name)
+ f.size = append(f.size, size)
+ f.inArray = append(f.inArray, array)
+}
+
+func (f *JSONFormatter) pop() {
+ f.path = f.path[0 : len(f.path)-1]
+ f.size = f.size[0 : len(f.size)-1]
+ f.inArray = f.inArray[0 : len(f.inArray)-1]
+}
+
+func (f *JSONFormatter) addLineWith(change ChangeType, value string) {
+ f.line = &AsciiLine{
+ change: change,
+ indent: len(f.path),
+ buffer: bytes.NewBufferString(value),
+ }
+ f.closeLine()
+}
+
+func (f *JSONFormatter) newLine(change ChangeType) {
+ f.line = &AsciiLine{
+ change: change,
+ indent: len(f.path),
+ buffer: bytes.NewBuffer([]byte{}),
+ }
+}
+
+func (f *JSONFormatter) closeLine() {
+ leftLine := 0
+ rightLine := 0
+ f.lineCount++
+
+ switch f.line.change {
+ case ChangeAdded, ChangeNew:
+ f.rightLine++
+ rightLine = f.rightLine
+
+ case ChangeDeleted, ChangeOld:
+ f.leftLine++
+ leftLine = f.leftLine
+
+ case ChangeNil, ChangeUnchanged:
+ f.rightLine++
+ f.leftLine++
+ rightLine = f.rightLine
+ leftLine = f.leftLine
+ }
+
+ s := f.line.buffer.String()
+ f.Lines = append(f.Lines, &JSONLine{
+ LineNum: f.lineCount,
+ RightLine: rightLine,
+ LeftLine: leftLine,
+ Indent: f.line.indent,
+ Text: s,
+ Change: f.line.change,
+ Key: f.line.key,
+ Val: f.line.val,
+ })
+}
+
+func (f *JSONFormatter) printKey(name string) {
+ if !f.inArray[len(f.inArray)-1] {
+ f.line.key = name
+ fmt.Fprintf(f.line.buffer, `"%s": `, name)
+ }
+}
+
+func (f *JSONFormatter) printComma() {
+ f.size[len(f.size)-1]--
+ if f.size[len(f.size)-1] > 0 {
+ f.line.buffer.WriteRune(',')
+ }
+}
+
+func (f *JSONFormatter) printValue(value interface{}) {
+ switch value.(type) {
+ case string:
+ f.line.val = value
+ fmt.Fprintf(f.line.buffer, `"%s"`, value)
+ case nil:
+ f.line.val = "null"
+ f.line.buffer.WriteString("null")
+ default:
+ f.line.val = value
+ fmt.Fprintf(f.line.buffer, `%#v`, value)
+ }
+}
+
+func (f *JSONFormatter) print(a string) {
+ f.line.buffer.WriteString(a)
+}
+
+func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
+ switch value.(type) {
+ case map[string]interface{}:
+ f.newLine(change)
+ f.printKey(name)
+ f.print("{")
+ f.closeLine()
+
+ m := value.(map[string]interface{})
+ size := len(m)
+ f.push(name, size, false)
+
+ keys := sortKeys(m)
+ for _, key := range keys {
+ f.printRecursive(key, m[key], change)
+ }
+ f.pop()
+
+ f.newLine(change)
+ f.print("}")
+ f.printComma()
+ f.closeLine()
+
+ case []interface{}:
+ f.newLine(change)
+ f.printKey(name)
+ f.print("[")
+ f.closeLine()
+
+ s := value.([]interface{})
+ size := len(s)
+ f.push("", size, true)
+ for _, item := range s {
+ f.printRecursive("", item, change)
+ }
+ f.pop()
+
+ f.newLine(change)
+ f.print("]")
+ f.printComma()
+ f.closeLine()
+
+ default:
+ f.newLine(change)
+ f.printKey(name)
+ f.printValue(value)
+ f.printComma()
+ f.closeLine()
+ }
+}
+
+func sortKeys(m map[string]interface{}) (keys []string) {
+ keys = make([]string, 0, len(m))
+ for key := range m {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ return
+}
diff --git a/pkg/models/dashboard_version.go b/pkg/models/dashboard_version.go
new file mode 100644
index 00000000000..06b5797e57c
--- /dev/null
+++ b/pkg/models/dashboard_version.go
@@ -0,0 +1,71 @@
+package models
+
+import (
+ "errors"
+ "time"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+)
+
+var (
+ ErrDashboardVersionNotFound = errors.New("Dashboard version not found")
+ ErrNoVersionsForDashboardId = errors.New("No dashboard versions found for the given DashboardId")
+)
+
+// A DashboardVersion represents the comparable data in a dashboard, allowing
+// diffs of the dashboard to be performed.
+type DashboardVersion struct {
+ Id int64 `json:"id"`
+ DashboardId int64 `json:"dashboardId"`
+ ParentVersion int `json:"parentVersion"`
+ RestoredFrom int `json:"restoredFrom"`
+ Version int `json:"version"`
+
+ Created time.Time `json:"created"`
+ CreatedBy int64 `json:"createdBy"`
+
+ Message string `json:"message"`
+ Data *simplejson.Json `json:"data"`
+}
+
+// DashboardVersionMeta extends the dashboard version model with the names
+// associated with the UserIds, overriding the field with the same name from
+// the DashboardVersion model.
+type DashboardVersionMeta struct {
+ DashboardVersion
+ CreatedBy string `json:"createdBy"`
+}
+
+// DashboardVersionDTO represents a dashboard version, without the dashboard
+// map.
+type DashboardVersionDTO struct {
+ Id int64 `json:"id"`
+ DashboardId int64 `json:"dashboardId"`
+ ParentVersion int `json:"parentVersion"`
+ RestoredFrom int `json:"restoredFrom"`
+ Version int `json:"version"`
+ Created time.Time `json:"created"`
+ CreatedBy string `json:"createdBy"`
+ Message string `json:"message"`
+}
+
+//
+// Queries
+//
+
+type GetDashboardVersionQuery struct {
+ DashboardId int64
+ OrgId int64
+ Version int
+
+ Result *DashboardVersion
+}
+
+type GetDashboardVersionsQuery struct {
+ DashboardId int64
+ OrgId int64
+ Limit int
+ Start int
+
+ Result []*DashboardVersionDTO
+}
diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go
index 634b26c3f29..0463e9c209b 100644
--- a/pkg/models/dashboards.go
+++ b/pkg/models/dashboards.go
@@ -98,12 +98,17 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
// GetDashboardModel turns the command into the savable model
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash := NewDashboardFromJson(cmd.Dashboard)
+ userId := cmd.UserId
- if dash.Data.Get("version").MustInt(0) == 0 {
- dash.CreatedBy = cmd.UserId
+ if userId == 0 {
+ userId = -1
}
- dash.UpdatedBy = cmd.UserId
+ if dash.Data.Get("version").MustInt(0) == 0 {
+ dash.CreatedBy = userId
+ }
+
+ dash.UpdatedBy = userId
dash.OrgId = cmd.OrgId
dash.PluginId = cmd.PluginId
dash.UpdateSlug()
@@ -126,11 +131,13 @@ func (dash *Dashboard) UpdateSlug() {
//
type SaveDashboardCommand struct {
- Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
- UserId int64 `json:"userId"`
- OrgId int64 `json:"-"`
- Overwrite bool `json:"overwrite"`
- PluginId string `json:"-"`
+ Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
+ UserId int64 `json:"userId"`
+ Overwrite bool `json:"overwrite"`
+ Message string `json:"message"`
+ OrgId int64 `json:"-"`
+ RestoredFrom int `json:"-"`
+ PluginId string `json:"-"`
Result *Dashboard
}
@@ -145,7 +152,8 @@ type DeleteDashboardCommand struct {
//
type GetDashboardQuery struct {
- Slug string
+ Slug string // required if no Id is specified
+ Id int64 // optional if slug is set
OrgId int64
Result *Dashboard
diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go
index d8e9ec34890..50b02bf0970 100644
--- a/pkg/services/sqlstore/dashboard.go
+++ b/pkg/services/sqlstore/dashboard.go
@@ -3,6 +3,7 @@ package sqlstore
import (
"bytes"
"fmt"
+ "time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics"
@@ -62,16 +63,20 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
if dash.Id != sameTitle.Id {
if cmd.Overwrite {
dash.Id = sameTitle.Id
+ dash.Version = sameTitle.Version
} else {
return m.ErrDashboardWithSameNameExists
}
}
}
+ parentVersion := dash.Version
affectedRows := int64(0)
if dash.Id == 0 {
+ dash.Version = 1
metrics.M_Models_Dashboard_Insert.Inc(1)
+ dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Insert(dash)
} else {
dash.Version += 1
@@ -79,10 +84,32 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
affectedRows, err = sess.Id(dash.Id).Update(dash)
}
+ if err != nil {
+ return err
+ }
+
if affectedRows == 0 {
return m.ErrDashboardNotFound
}
+ dashVersion := &m.DashboardVersion{
+ DashboardId: dash.Id,
+ ParentVersion: parentVersion,
+ RestoredFrom: cmd.RestoredFrom,
+ Version: dash.Version,
+ Created: time.Now(),
+ CreatedBy: dash.UpdatedBy,
+ Message: cmd.Message,
+ Data: dash.Data,
+ }
+
+ // insert version entry
+ if affectedRows, err = sess.Insert(dashVersion); err != nil {
+ return err
+ } else if affectedRows == 0 {
+ return m.ErrDashboardNotFound
+ }
+
// delete existing tabs
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
if err != nil {
@@ -106,8 +133,9 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
}
func GetDashboard(query *m.GetDashboardQuery) error {
- dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId}
+ dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&dashboard)
+
if err != nil {
return err
} else if has == false {
@@ -116,7 +144,6 @@ func GetDashboard(query *m.GetDashboardQuery) error {
dashboard.Data.Set("id", dashboard.Id)
query.Result = &dashboard
-
return nil
}
@@ -233,6 +260,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
"DELETE FROM star WHERE dashboard_id = ? ",
"DELETE FROM dashboard WHERE id = ?",
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
+ "DELETE FROM dashboard_version WHERE dashboard_id = ?",
}
for _, sql := range deletes {
diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go
new file mode 100644
index 00000000000..14924839b53
--- /dev/null
+++ b/pkg/services/sqlstore/dashboard_version.go
@@ -0,0 +1,59 @@
+package sqlstore
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+ bus.AddHandler("sql", GetDashboardVersion)
+ bus.AddHandler("sql", GetDashboardVersions)
+}
+
+// GetDashboardVersion gets the dashboard version for the given dashboard ID and version number.
+func GetDashboardVersion(query *m.GetDashboardVersionQuery) error {
+ version := m.DashboardVersion{}
+ has, err := x.Where("dashboard_version.dashboard_id=? AND dashboard_version.version=? AND dashboard.org_id=?", query.DashboardId, query.Version, query.OrgId).
+ Join("LEFT", "dashboard", `dashboard.id = dashboard_version.dashboard_id`).
+ Get(&version)
+
+ if err != nil {
+ return err
+ }
+
+ if !has {
+ return m.ErrDashboardVersionNotFound
+ }
+
+ query.Result = &version
+ return nil
+}
+
+// GetDashboardVersions gets all dashboard versions for the given dashboard ID.
+func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
+ err := x.Table("dashboard_version").
+ Select(`dashboard_version.id,
+ dashboard_version.dashboard_id,
+ dashboard_version.parent_version,
+ dashboard_version.restored_from,
+ dashboard_version.version,
+ dashboard_version.created,
+ dashboard_version.created_by as created_by_id,
+ dashboard_version.message,
+ dashboard_version.data,`+
+ dialect.Quote("user")+`.login as created_by`).
+ Join("LEFT", "user", `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
+ Join("LEFT", "dashboard", `dashboard.id = dashboard_version.dashboard_id`).
+ Where("dashboard_version.dashboard_id=? AND dashboard.org_id=?", query.DashboardId, query.OrgId).
+ OrderBy("dashboard_version.version DESC").
+ Limit(query.Limit, query.Start).
+ Find(&query.Result)
+ if err != nil {
+ return err
+ }
+
+ if len(query.Result) < 1 {
+ return m.ErrNoVersionsForDashboardId
+ }
+ return nil
+}
diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go
new file mode 100644
index 00000000000..12f23cdb54a
--- /dev/null
+++ b/pkg/services/sqlstore/dashboard_version_test.go
@@ -0,0 +1,102 @@
+package sqlstore
+
+import (
+ "reflect"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) {
+ data["title"] = dashboard.Title
+
+ saveCmd := m.SaveDashboardCommand{
+ OrgId: dashboard.OrgId,
+ Overwrite: true,
+ Dashboard: simplejson.NewFromAny(data),
+ }
+
+ err := SaveDashboard(&saveCmd)
+ So(err, ShouldBeNil)
+}
+
+func TestGetDashboardVersion(t *testing.T) {
+ Convey("Testing dashboard version retrieval", t, func() {
+ InitTestDB(t)
+
+ Convey("Get a Dashboard ID and version ID", func() {
+ savedDash := insertTestDashboard("test dash 26", 1, "diff")
+
+ query := m.GetDashboardVersionQuery{
+ DashboardId: savedDash.Id,
+ Version: savedDash.Version,
+ OrgId: 1,
+ }
+
+ err := GetDashboardVersion(&query)
+ So(err, ShouldBeNil)
+ So(savedDash.Id, ShouldEqual, query.DashboardId)
+ So(savedDash.Version, ShouldEqual, query.Version)
+
+ dashCmd := m.GetDashboardQuery{
+ OrgId: savedDash.OrgId,
+ Slug: savedDash.Slug,
+ }
+ err = GetDashboard(&dashCmd)
+ So(err, ShouldBeNil)
+ eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data)
+ So(eq, ShouldEqual, true)
+ })
+
+ Convey("Attempt to get a version that doesn't exist", func() {
+ query := m.GetDashboardVersionQuery{
+ DashboardId: int64(999),
+ Version: 123,
+ OrgId: 1,
+ }
+
+ err := GetDashboardVersion(&query)
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, m.ErrDashboardVersionNotFound)
+ })
+ })
+}
+
+func TestGetDashboardVersions(t *testing.T) {
+ Convey("Testing dashboard versions retrieval", t, func() {
+ InitTestDB(t)
+ savedDash := insertTestDashboard("test dash 43", 1, "diff-all")
+
+ Convey("Get all versions for a given Dashboard ID", func() {
+ query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
+
+ err := GetDashboardVersions(&query)
+ So(err, ShouldBeNil)
+ So(len(query.Result), ShouldEqual, 1)
+ })
+
+ Convey("Attempt to get the versions for a non-existent Dashboard ID", func() {
+ query := m.GetDashboardVersionsQuery{DashboardId: int64(999), OrgId: 1}
+
+ err := GetDashboardVersions(&query)
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, m.ErrNoVersionsForDashboardId)
+ So(len(query.Result), ShouldEqual, 0)
+ })
+
+ Convey("Get all versions for an updated dashboard", func() {
+ updateTestDashboard(savedDash, map[string]interface{}{
+ "tags": "different-tag",
+ })
+
+ query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
+ err := GetDashboardVersions(&query)
+
+ So(err, ShouldBeNil)
+ So(len(query.Result), ShouldEqual, 2)
+ })
+ })
+}
diff --git a/pkg/services/sqlstore/logger.go b/pkg/services/sqlstore/logger.go
index ae1145c21b0..9b0b068c918 100644
--- a/pkg/services/sqlstore/logger.go
+++ b/pkg/services/sqlstore/logger.go
@@ -23,67 +23,59 @@ func NewXormLogger(level glog.Lvl, grafanaLog glog.Logger) *XormLogger {
}
// Error implement core.ILogger
-func (s *XormLogger) Err(v ...interface{}) error {
+func (s *XormLogger) Error(v ...interface{}) {
if s.level <= glog.LvlError {
s.grafanaLog.Error(fmt.Sprint(v...))
}
- return nil
}
// Errorf implement core.ILogger
-func (s *XormLogger) Errf(format string, v ...interface{}) error {
+func (s *XormLogger) Errorf(format string, v ...interface{}) {
if s.level <= glog.LvlError {
s.grafanaLog.Error(fmt.Sprintf(format, v...))
}
- return nil
}
// Debug implement core.ILogger
-func (s *XormLogger) Debug(v ...interface{}) error {
+func (s *XormLogger) Debug(v ...interface{}) {
if s.level <= glog.LvlDebug {
s.grafanaLog.Debug(fmt.Sprint(v...))
}
- return nil
}
// Debugf implement core.ILogger
-func (s *XormLogger) Debugf(format string, v ...interface{}) error {
+func (s *XormLogger) Debugf(format string, v ...interface{}) {
if s.level <= glog.LvlDebug {
s.grafanaLog.Debug(fmt.Sprintf(format, v...))
}
- return nil
}
// Info implement core.ILogger
-func (s *XormLogger) Info(v ...interface{}) error {
+func (s *XormLogger) Info(v ...interface{}) {
if s.level <= glog.LvlInfo {
s.grafanaLog.Info(fmt.Sprint(v...))
}
- return nil
}
// Infof implement core.ILogger
-func (s *XormLogger) Infof(format string, v ...interface{}) error {
+func (s *XormLogger) Infof(format string, v ...interface{}) {
if s.level <= glog.LvlInfo {
s.grafanaLog.Info(fmt.Sprintf(format, v...))
}
- return nil
}
// Warn implement core.ILogger
-func (s *XormLogger) Warning(v ...interface{}) error {
+func (s *XormLogger) Warn(v ...interface{}) {
if s.level <= glog.LvlWarn {
s.grafanaLog.Warn(fmt.Sprint(v...))
}
- return nil
}
// Warnf implement core.ILogger
-func (s *XormLogger) Warningf(format string, v ...interface{}) error {
+func (s *XormLogger) Warnf(format string, v ...interface{}) {
if s.level <= glog.LvlWarn {
s.grafanaLog.Warn(fmt.Sprintf(format, v...))
}
- return nil
}
// Level implement core.ILogger
@@ -103,8 +95,7 @@ func (s *XormLogger) Level() core.LogLevel {
}
// SetLevel implement core.ILogger
-func (s *XormLogger) SetLevel(l core.LogLevel) error {
- return nil
+func (s *XormLogger) SetLevel(l core.LogLevel) {
}
// ShowSQL implement core.ILogger
diff --git a/pkg/services/sqlstore/migrations/dashboard_version_mig.go b/pkg/services/sqlstore/migrations/dashboard_version_mig.go
new file mode 100644
index 00000000000..fee69b9ef4c
--- /dev/null
+++ b/pkg/services/sqlstore/migrations/dashboard_version_mig.go
@@ -0,0 +1,61 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addDashboardVersionMigration(mg *Migrator) {
+ dashboardVersionV1 := Table{
+ Name: "dashboard_version",
+ Columns: []*Column{
+ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+ {Name: "dashboard_id", Type: DB_BigInt},
+ {Name: "parent_version", Type: DB_Int, Nullable: false},
+ {Name: "restored_from", Type: DB_Int, Nullable: false},
+ {Name: "version", Type: DB_Int, Nullable: false},
+ {Name: "created", Type: DB_DateTime, Nullable: false},
+ {Name: "created_by", Type: DB_BigInt, Nullable: false},
+ {Name: "message", Type: DB_Text, Nullable: false},
+ {Name: "data", Type: DB_Text, Nullable: false},
+ },
+ Indices: []*Index{
+ {Cols: []string{"dashboard_id"}},
+ {Cols: []string{"dashboard_id", "version"}, Type: UniqueIndex},
+ },
+ }
+
+ mg.AddMigration("create dashboard_version table v1", NewAddTableMigration(dashboardVersionV1))
+ mg.AddMigration("add index dashboard_version.dashboard_id", NewAddIndexMigration(dashboardVersionV1, dashboardVersionV1.Indices[0]))
+ mg.AddMigration("add unique index dashboard_version.dashboard_id and dashboard_version.version", NewAddIndexMigration(dashboardVersionV1, dashboardVersionV1.Indices[1]))
+
+ // before new dashboards where created with version 0, now they are always inserted with version 1
+ const setVersionTo1WhereZeroSQL = `UPDATE dashboard SET version = 1 WHERE version = 0`
+ mg.AddMigration("Set dashboard version to 1 where 0", new(RawSqlMigration).
+ Sqlite(setVersionTo1WhereZeroSQL).
+ Postgres(setVersionTo1WhereZeroSQL).
+ Mysql(setVersionTo1WhereZeroSQL))
+
+ const rawSQL = `INSERT INTO dashboard_version
+(
+ dashboard_id,
+ version,
+ parent_version,
+ restored_from,
+ created,
+ created_by,
+ message,
+ data
+)
+SELECT
+ dashboard.id,
+ dashboard.version,
+ dashboard.version,
+ dashboard.version,
+ dashboard.updated,
+ dashboard.updated_by,
+ '',
+ dashboard.data
+FROM dashboard;`
+ mg.AddMigration("save existing dashboard data in dashboard_version table v1", new(RawSqlMigration).
+ Sqlite(rawSQL).
+ Postgres(rawSQL).
+ Mysql(rawSQL))
+}
diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go
index bf334d57bb0..38072fe88e4 100644
--- a/pkg/services/sqlstore/migrations/migrations.go
+++ b/pkg/services/sqlstore/migrations/migrations.go
@@ -25,6 +25,7 @@ func AddMigrations(mg *Migrator) {
addAlertMigrations(mg)
addAnnotationMig(mg)
addTestDataMigrations(mg)
+ addDashboardVersionMigration(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts
index 889398d5138..371b1ffe112 100644
--- a/public/app/core/components/switch.ts
+++ b/public/app/core/components/switch.ts
@@ -7,7 +7,7 @@ import coreModule from 'app/core/core_module';
import Drop from 'tether-drop';
var template = `
-