Files
grafana/vendor/github.com/yudai/gojsondiff/deltas.go
T
Ben Tranter b6e46c9eb8 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.
2017-05-24 19:14:39 -04:00

462 lines
11 KiB
Go

package gojsondiff
import (
"errors"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"reflect"
"strconv"
)
// A Delta represents an atomic difference between two JSON objects.
type Delta interface {
// Similarity calculates the similarity of the Delta values.
// The return value is normalized from 0 to 1,
// 0 is completely different and 1 is they are same
Similarity() (similarity float64)
}
// To cache the calculated similarity,
// concrete Deltas can use similariter and similarityCache
type similariter interface {
similarity() (similarity float64)
}
type similarityCache struct {
similariter
value float64
}
func newSimilarityCache(sim similariter) similarityCache {
cache := similarityCache{similariter: sim, value: -1}
return cache
}
func (cache similarityCache) Similarity() (similarity float64) {
if cache.value < 0 {
cache.value = cache.similariter.similarity()
}
return cache.value
}
// A Position represents the position of a Delta in an object or an array.
type Position interface {
// String returns the position as a string
String() (name string)
// CompareTo returns a true if the Position is smaller than another Position.
// This function is used to sort Positions by the sort package.
CompareTo(another Position) bool
}
// A Name is a Postition with a string, which means the delta is in an object.
type Name string
func (n Name) String() (name string) {
return string(n)
}
func (n Name) CompareTo(another Position) bool {
return n < another.(Name)
}
// A Index is a Position with an int value, which means the Delta is in an Array.
type Index int
func (i Index) String() (name string) {
return strconv.Itoa(int(i))
}
func (i Index) CompareTo(another Position) bool {
return i < another.(Index)
}
// A PreDelta is a Delta that has a position of the left side JSON object.
// Deltas implements this interface should be applies before PostDeltas.
type PreDelta interface {
// PrePosition returns the Position.
PrePosition() Position
// PreApply applies the delta to object.
PreApply(object interface{}) interface{}
}
type preDelta struct{ Position }
func (i preDelta) PrePosition() Position {
return Position(i.Position)
}
type preDeltas []PreDelta
// for sorting
func (s preDeltas) Len() int {
return len(s)
}
// for sorting
func (s preDeltas) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// for sorting
func (s preDeltas) Less(i, j int) bool {
return !s[i].PrePosition().CompareTo(s[j].PrePosition())
}
// A PreDelta is a Delta that has a position of the right side JSON object.
// Deltas implements this interface should be applies after PreDeltas.
type PostDelta interface {
// PostPosition returns the Position.
PostPosition() Position
// PostApply applies the delta to object.
PostApply(object interface{}) interface{}
}
type postDelta struct{ Position }
func (i postDelta) PostPosition() Position {
return Position(i.Position)
}
type postDeltas []PostDelta
// for sorting
func (s postDeltas) Len() int {
return len(s)
}
// for sorting
func (s postDeltas) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// for sorting
func (s postDeltas) Less(i, j int) bool {
return s[i].PostPosition().CompareTo(s[j].PostPosition())
}
// An Object is a Delta that represents an object of JSON
type Object struct {
postDelta
similarityCache
// Deltas holds internal Deltas
Deltas []Delta
}
// NewObject returns an Object
func NewObject(position Position, deltas []Delta) *Object {
d := Object{postDelta: postDelta{position}, Deltas: deltas}
d.similarityCache = newSimilarityCache(&d)
return &d
}
func (d *Object) PostApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
o := object.(map[string]interface{})
n := string(d.PostPosition().(Name))
o[n] = applyDeltas(d.Deltas, o[n])
case []interface{}:
o := object.([]interface{})
n := int(d.PostPosition().(Index))
o[n] = applyDeltas(d.Deltas, o[n])
}
return object
}
func (d *Object) similarity() (similarity float64) {
similarity = deltasSimilarity(d.Deltas)
return
}
// An Array is a Delta that represents an array of JSON
type Array struct {
postDelta
similarityCache
// Deltas holds internal Deltas
Deltas []Delta
}
// NewArray returns an Array
func NewArray(position Position, deltas []Delta) *Array {
d := Array{postDelta: postDelta{position}, Deltas: deltas}
d.similarityCache = newSimilarityCache(&d)
return &d
}
func (d *Array) PostApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
o := object.(map[string]interface{})
n := string(d.PostPosition().(Name))
o[n] = applyDeltas(d.Deltas, o[n])
case []interface{}:
o := object.([]interface{})
n := int(d.PostPosition().(Index))
o[n] = applyDeltas(d.Deltas, o[n])
}
return object
}
func (d *Array) similarity() (similarity float64) {
similarity = deltasSimilarity(d.Deltas)
return
}
// An Added represents a new added field of an object or an array
type Added struct {
postDelta
similarityCache
// Values holds the added value
Value interface{}
}
// NewAdded returns a new Added
func NewAdded(position Position, value interface{}) *Added {
d := Added{postDelta: postDelta{position}, Value: value}
return &d
}
func (d *Added) PostApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
object.(map[string]interface{})[string(d.PostPosition().(Name))] = d.Value
case []interface{}:
i := int(d.PostPosition().(Index))
o := object.([]interface{})
if i < len(o) {
o = append(o, 0) //dummy
copy(o[i+1:], o[i:])
o[i] = d.Value
object = o
} else {
object = append(o, d.Value)
}
}
return object
}
func (d *Added) similarity() (similarity float64) {
return 0
}
// A Modified represents a field whose value is changed.
type Modified struct {
postDelta
similarityCache
// The value before modification
OldValue interface{}
// The value after modification
NewValue interface{}
}
// NewModified returns a Modified
func NewModified(position Position, oldValue, newValue interface{}) *Modified {
d := Modified{
postDelta: postDelta{position},
OldValue: oldValue,
NewValue: newValue,
}
d.similarityCache = newSimilarityCache(&d)
return &d
}
func (d *Modified) PostApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
// TODO check old value
object.(map[string]interface{})[string(d.PostPosition().(Name))] = d.NewValue
case []interface{}:
object.([]interface{})[int(d.PostPosition().(Index))] = d.NewValue
}
return object
}
func (d *Modified) similarity() (similarity float64) {
similarity += 0.3 // at least, they are at the same position
if reflect.TypeOf(d.OldValue) == reflect.TypeOf(d.NewValue) {
similarity += 0.3 // types are same
switch d.OldValue.(type) {
case string:
similarity += 0.4 * stringSimilarity(d.OldValue.(string), d.NewValue.(string))
case float64:
ratio := d.OldValue.(float64) / d.NewValue.(float64)
if ratio > 1 {
ratio = 1 / ratio
}
similarity += 0.4 * ratio
}
}
return
}
// A TextDiff represents a Modified with TextDiff between the old and the new values.
type TextDiff struct {
Modified
// Diff string
Diff []dmp.Patch
}
// NewTextDiff returns
func NewTextDiff(position Position, diff []dmp.Patch, oldValue, newValue interface{}) *TextDiff {
d := TextDiff{
Modified: *NewModified(position, oldValue, newValue),
Diff: diff,
}
return &d
}
func (d *TextDiff) PostApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
o := object.(map[string]interface{})
i := string(d.PostPosition().(Name))
// TODO error
d.OldValue = o[i]
// TODO error
d.patch()
o[i] = d.NewValue
case []interface{}:
o := object.([]interface{})
i := d.PostPosition().(Index)
d.OldValue = o[i]
// TODO error
d.patch()
o[i] = d.NewValue
}
return object
}
func (d *TextDiff) patch() error {
if d.OldValue == nil {
return errors.New("Old Value is not set")
}
patcher := dmp.New()
patched, successes := patcher.PatchApply(d.Diff, d.OldValue.(string))
for _, success := range successes {
if !success {
return errors.New("Failed to apply a patch")
}
}
d.NewValue = patched
return nil
}
func (d *TextDiff) DiffString() string {
dmp := dmp.New()
return dmp.PatchToText(d.Diff)
}
// A Delted represents deleted field or index of an Object or an Array.
type Deleted struct {
preDelta
// The value deleted
Value interface{}
}
// NewDeleted returns a Deleted
func NewDeleted(position Position, value interface{}) *Deleted {
d := Deleted{
preDelta: preDelta{position},
Value: value,
}
return &d
}
func (d *Deleted) PreApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
// TODO check old value
delete(object.(map[string]interface{}), string(d.PrePosition().(Name)))
case []interface{}:
i := int(d.PrePosition().(Index))
o := object.([]interface{})
object = append(o[:i], o[i+1:]...)
}
return object
}
func (d Deleted) Similarity() (similarity float64) {
return 0
}
// A Moved represents field that is moved, which means the index or name is
// changed. Note that, in this library, assigning a Moved and a Modified to
// a single position is not allowed. For the compatibility with jsondiffpatch,
// the Moved in this library can hold the old and new value in it.
type Moved struct {
preDelta
postDelta
similarityCache
// The value before moving
Value interface{}
// The delta applied after moving (for compatibility)
Delta interface{}
}
func NewMoved(oldPosition Position, newPosition Position, value interface{}, delta Delta) *Moved {
d := Moved{
preDelta: preDelta{oldPosition},
postDelta: postDelta{newPosition},
Value: value,
Delta: delta,
}
d.similarityCache = newSimilarityCache(&d)
return &d
}
func (d *Moved) PreApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
//not supported
case []interface{}:
i := int(d.PrePosition().(Index))
o := object.([]interface{})
d.Value = o[i]
object = append(o[:i], o[i+1:]...)
}
return object
}
func (d *Moved) PostApply(object interface{}) interface{} {
switch object.(type) {
case map[string]interface{}:
//not supported
case []interface{}:
i := int(d.PostPosition().(Index))
o := object.([]interface{})
o = append(o, 0) //dummy
copy(o[i+1:], o[i:])
o[i] = d.Value
object = o
}
if d.Delta != nil {
d.Delta.(PostDelta).PostApply(object)
}
return object
}
func (d *Moved) similarity() (similarity float64) {
similarity = 0.6 // as type and contens are same
ratio := float64(d.PrePosition().(Index)) / float64(d.PostPosition().(Index))
if ratio > 1 {
ratio = 1 / ratio
}
similarity += 0.4 * ratio
return
}