History and Version Control for Dashboard Updates
A simple version control system for dashboards. Closes #1504. Goals 1. To create a new dashboard version every time a dashboard is saved. 2. To allow users to view all versions of a given dashboard. 3. To allow users to rollback to a previous version of a dashboard. 4. To allow users to compare two versions of a dashboard. Usage Navigate to a dashboard, and click the settings cog. From there, click the "Changelog" button to be brought to the Changelog view. In this view, a table containing each version of a dashboard can be seen. Each entry in the table represents a dashboard version. A selectable checkbox, the version number, date created, name of the user who created that version, and commit message is shown in the table, along with a button that allows a user to restore to a previous version of that dashboard. If a user wants to restore to a previous version of their dashboard, they can do so by clicking the previously mentioned button. If a user wants to compare two different versions of a dashboard, they can do so by clicking the checkbox of two different dashboard versions, then clicking the "Compare versions" button located below the dashboard. From there, the user is brought to a view showing a summary of the dashboard differences. Each summarized change contains a link that can be clicked to take the user a JSON diff highlighting the changes line by line. Overview of Changes Backend Changes - A `dashboard_version` table was created to store each dashboard version, along with a dashboard version model and structs to represent the queries and commands necessary for the dashboard version API methods. - API endpoints were created to support working with dashboard versions. - Methods were added to create, update, read, and destroy dashboard versions in the database. - Logic was added to compute the diff between two versions, and display it to the user. - The dashboard migration logic was updated to save a "Version 1" of each existing dashboard in the database. Frontend Changes - New views - Methods to pull JSON and HTML from endpoints New API Endpoints Each endpoint requires the authorization header to be sent in the format, ``` Authorization: Bearer <jwt> ``` where `<jwt>` is a JSON web token obtained from the Grafana admin panel. `GET "/api/dashboards/db/:dashboardId/versions?orderBy=<string>&limit=<int>&start=<int>"` Get all dashboard versions for the given dashboard ID. Accepts three URL parameters: - `orderBy` String to order the results by. Possible values are `version`, `created`, `created_by`, `message`. Default is `versions`. Ordering is always in descending order. - `limit` Maximum number of results to return - `start` Position in results to start from `GET "/api/dashboards/db/:dashboardId/versions/:id"` Get an individual dashboard version by ID, for the given dashboard ID. `POST "/api/dashboards/db/:dashboardId/restore"` Restore to the given dashboard version. Post body is of content-type `application/json`, and must contain. ```json { "dashboardId": <int>, "version": <int> } ``` `GET "/api/dashboards/db/:dashboardId/compare/:versionA...:versionB"` Compare two dashboard versions by ID for the given dashboard ID, returning a JSON delta formatted representation of the diff. The URL format follows what GitHub does. For example, visiting [/api/dashboards/db/18/compare/22...33](http://ec2-54-80-139-44.compute-1.amazonaws.com:3000/api/dashboards/db/18/compare/22...33) will return the diff between versions 22 and 33 for the dashboard ID 18. Dependencies Added - The Go package [gojsondiff](https://github.com/yudai/gojsondiff) was added and vendored.
This commit is contained in:
committed by
Carlos Rosquillas
parent
59f3cca135
commit
b6e46c9eb8
@@ -0,0 +1,334 @@
|
||||
package formatter
|
||||
|
||||
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 {
|
||||
if b.LastIndent == 3 && line.Indent == 2 && line.Change == ChangeNil {
|
||||
if b.Block != nil {
|
||||
blocks = append(blocks, b.Block)
|
||||
}
|
||||
}
|
||||
b.LastIndent = line.Indent
|
||||
|
||||
if line.Indent == 2 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Other Lines
|
||||
if line.Indent > 2 {
|
||||
// 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 . }}
|
||||
<div class="diff-group">
|
||||
<div class="diff-block">
|
||||
<h2 class="diff-block-title">
|
||||
<i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle"></i>
|
||||
<strong class="diff-title">{{ .Title }}</strong> {{ getChange .Change }}
|
||||
</h2>
|
||||
|
||||
|
||||
<!-- Overview -->
|
||||
{{ if .Old }}
|
||||
<div class="change list-change diff-label">{{ .Old }}</div>
|
||||
<i class="diff-arrow fa fa-long-arrow-right"></i>
|
||||
{{ end }}
|
||||
{{ if .New }}
|
||||
<div class="change list-change diff-label">{{ .New }}</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .LineStart }}
|
||||
<diff-link-json
|
||||
line-link="{{ .LineStart }}"
|
||||
line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
|
||||
switch-view="ctrl.getDiff('html')"
|
||||
/>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Basic Changes -->
|
||||
{{ range .Changes }}
|
||||
<ul class="diff-change-container">
|
||||
{{ template "change" . }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
<!-- Basic Summary -->
|
||||
{{ range .Summaries }}
|
||||
{{ template "summary" . }}
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}`
|
||||
|
||||
// tplChange is the template for changes
|
||||
tplChange = `{{ define "change" -}}
|
||||
<li class="diff-change-group">
|
||||
<span class="bullet-position-container">
|
||||
<div class="diff-change-item diff-change-title">{{ getChange .Change }} {{ .Key }}</div>
|
||||
|
||||
<div class="diff-change-item">
|
||||
{{ if .Old }}
|
||||
<div class="change list-change diff-label">{{ .Old }}</div>
|
||||
<i class="diff-arrow fa fa-long-arrow-right"></i>
|
||||
{{ end }}
|
||||
{{ if .New }}
|
||||
<div class="change list-change diff-label">{{ .New }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ if .LineStart }}
|
||||
<diff-link-json
|
||||
line-link="{{ .LineStart }}"
|
||||
line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
|
||||
switch-view="ctrl.getDiff('html')"
|
||||
/>
|
||||
{{ end }}
|
||||
</span>
|
||||
</li>
|
||||
{{ end }}`
|
||||
|
||||
// tplSummary is for basis summaries
|
||||
tplSummary = `{{ define "summary" -}}
|
||||
<div class="diff-group-name">
|
||||
<i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i>
|
||||
|
||||
{{ if .Count }}
|
||||
<strong>{{ .Count }}</strong>
|
||||
{{ end }}
|
||||
|
||||
{{ if .Key }}
|
||||
<strong class="diff-summary-key">{{ .Key }}</strong>
|
||||
{{ getChange .Change }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .LineStart }}
|
||||
<diff-link-json
|
||||
line-link="{{ .LineStart }}"
|
||||
line-display="{{ .LineStart }}{{ if .LineEnd }} - {{ .LineEnd }}{{ end }}"
|
||||
switch-view="ctrl.getDiff('html')"
|
||||
/>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}`
|
||||
)
|
||||
Reference in New Issue
Block a user