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 }} + + {{ 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 = ` -