From 71921586d8eeb84285cb9668a7bdce7b7aa5d377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Pinson?= Date: Wed, 2 Aug 2017 17:05:03 +0200 Subject: [PATCH] Add a compare API and view (#21) * Make compare API richer * Add compare view * Unified diff * Version chooser * Use a directive for syntax highlighting * Diff UI * Do not include empty diffs in resource_diff --- compare/compare.go | 51 ++++++++++++------ static/compare.html | 90 +++++++++++++++++++++++++++++++ static/index.html | 5 ++ static/sh_diff.js | 122 ++++++++++++++++++++++++++++++++++++++++++ static/sh_main.min.js | 4 ++ static/sh_ruby.min.js | 1 + static/sh_style.css | 66 +++++++++++++++++++++++ static/state.html | 4 +- static/terraboard.css | 2 +- static/terraboard.js | 60 +++++++++++++++++++++ types/compare.go | 7 ++- 11 files changed, 393 insertions(+), 19 deletions(-) create mode 100644 static/compare.html create mode 100644 static/sh_diff.js create mode 100644 static/sh_main.min.js create mode 100644 static/sh_ruby.min.js create mode 100644 static/sh_style.css diff --git a/compare/compare.go b/compare/compare.go index 9fcd5e03..c50762df 100644 --- a/compare/compare.go +++ b/compare/compare.go @@ -2,6 +2,7 @@ package compare import ( "fmt" + "sort" "strings" log "github.com/Sirupsen/logrus" @@ -19,14 +20,6 @@ func stateResources(state types.State) (res []string) { return } -// Return all attributes of a resource -func resourceAttributes(res types.Resource) (attrs []string) { - for _, a := range res.Attributes { - attrs = append(attrs, a.Key) - } - return -} - // Returns elements only in s1 func sliceDiff(s1, s2 []string) (diff []string) { for _, e1 := range s1 { @@ -73,6 +66,15 @@ func getResource(state types.State, key string) (res types.Resource) { return } +// Return all attributes of a resource +func resourceAttributes(res types.Resource) (attrs []string) { + for _, a := range res.Attributes { + attrs = append(attrs, a.Key) + } + sort.Strings(attrs) + return +} + func getResourceAttribute(res types.Resource, key string) (val string) { for _, attr := range res.Attributes { if attr.Key == key { @@ -84,8 +86,8 @@ func getResourceAttribute(res types.Resource, key string) (val string) { func formatResource(res types.Resource) (out string) { out = fmt.Sprintf("resource \"%s\" \"%s\" {\n", res.Type, res.Name) - for _, attr := range res.Attributes { - out += fmt.Sprintf(" %s = \"%s\"\n", attr.Key, attr.Value) + for _, attr := range resourceAttributes(res) { + out += fmt.Sprintf(" %s = \"%s\"\n", attr, getResourceAttribute(res, attr)) } out += "}\n" @@ -116,7 +118,7 @@ func compareResource(st1, st2 types.State, key string) (comp types.ResourceDiff) } // Compute unified diff - diff := difflib.ContextDiff{ + diff := difflib.UnifiedDiff{ A: difflib.SplitLines(formatResource(res1)), B: difflib.SplitLines(formatResource(res2)), FromFile: stateInfo(st1), @@ -124,7 +126,7 @@ func compareResource(st1, st2 types.State, key string) (comp types.ResourceDiff) Context: 3, Eol: "\n", } - result, _ := difflib.GetContextDiffString(diff) + result, _ := difflib.GetUnifiedDiffString(diff) comp.UnifiedDiff = result return @@ -137,8 +139,11 @@ func Compare(from, to types.State) (comp types.StateCompare, err error) { } fromResources := stateResources(from) comp.Stats.From = types.StateInfo{ + Path: from.Path, VersionID: from.Version.VersionID, ResourceCount: len(fromResources), + TFVersion: from.TFVersion, + Serial: from.Serial, } if to.Path == "" { @@ -147,17 +152,33 @@ func Compare(from, to types.State) (comp types.StateCompare, err error) { } toResources := stateResources(to) comp.Stats.To = types.StateInfo{ + Path: to.Path, VersionID: to.Version.VersionID, ResourceCount: len(toResources), + TFVersion: to.TFVersion, + Serial: to.Serial, + } + + // OnlyInOld + onlyInOld := sliceDiff(fromResources, toResources) + comp.Differences.OnlyInOld = make(map[string]string) + for _, r := range onlyInOld { + comp.Differences.OnlyInOld[r] = formatResource(getResource(from, r)) } - comp.Differences.OnlyInOld = sliceDiff(fromResources, toResources) - comp.Differences.OnlyInNew = sliceDiff(toResources, fromResources) + // OnlyInNew + onlyInNew := sliceDiff(toResources, fromResources) + comp.Differences.OnlyInNew = make(map[string]string) + for _, r := range onlyInNew { + comp.Differences.OnlyInNew[r] = formatResource(getResource(to, r)) + } comp.Differences.InBoth = sliceInter(toResources, fromResources) comp.Differences.ResourceDiff = make(map[string]types.ResourceDiff) for _, r := range comp.Differences.InBoth { - comp.Differences.ResourceDiff[r] = compareResource(to, from, r) + if c := compareResource(to, from, r); c.UnifiedDiff != "" { + comp.Differences.ResourceDiff[r] = c + } } log.WithFields(log.Fields{ diff --git a/static/compare.html b/static/compare.html new file mode 100644 index 00000000..cebfe40f --- /dev/null +++ b/static/compare.html @@ -0,0 +1,90 @@ +
+
+
+
+
+

+ From version +

+
+
    +
  • Terraform version: {{compare.stats.from.terraform_version}}
  • +
  • Serial: {{compare.stats.from.serial}}
  • +
  • Version:
  • +
  • Resource count: {{compare.stats.from.resource_count}}
  • +
+
+
+
+

+ To version +

+
+
    +
  • Terraform version: {{compare.stats.to.terraform_version}}
  • +
  • Serial: {{compare.stats.to.serial}}
  • +
  • Version:
  • +
  • Resource count: {{compare.stats.to.resource_count}}
  • +
+
+
+
+
+

{{compare.stats.from.path}}

+
+
+
+

+ + Differences + + {{differences}} +

+
+
+
+
+
{{resource}}
+ +
+
+
+
+
+ +
+
+
+
{{resource}}
+ +
+
+
+
+
+ +
+
+
+
{{resource}}
+ +
+
+
+
+
+
diff --git a/static/index.html b/static/index.html index 8d8d0056..7382f134 100644 --- a/static/index.html +++ b/static/index.html @@ -26,6 +26,11 @@ + + + + + diff --git a/static/sh_diff.js b/static/sh_diff.js new file mode 100644 index 00000000..12ba2d6b --- /dev/null +++ b/static/sh_diff.js @@ -0,0 +1,122 @@ +if (! this.sh_languages) { + this.sh_languages = {}; +} +sh_languages['diff'] = [ + [ + [ + /(?=^[-]{3})/g, + 'sh_oldfile', + 1, + 1 + ], + [ + /(?=^[*]{3})/g, + 'sh_oldfile', + 3, + 1 + ], + [ + /(?=^[\d])/g, + 'sh_difflines', + 6, + 1 + ] + ], + [ + [ + /^[-]{3}/g, + 'sh_oldfile', + 2 + ], + [ + /^[-]/g, + 'sh_oldfile', + 2 + ], + [ + /^[+]/g, + 'sh_newfile', + 2 + ], + [ + /^@@/g, + 'sh_difflines', + 2 + ] + ], + [ + [ + /$/g, + null, + -2 + ] + ], + [ + [ + /^[*]{3}[ \t]+[\d]/g, + 'sh_oldfile', + 4 + ], + [ + /^[*]{3}/g, + 'sh_oldfile', + 2 + ], + [ + /^[-]{3}[ \t]+[\d]/g, + 'sh_newfile', + 5 + ], + [ + /^[-]{3}/g, + 'sh_newfile', + 2 + ] + ], + [ + [ + /^[\s]/g, + 'sh_normal', + 2 + ], + [ + /(?=^[-]{3})/g, + 'sh_newfile', + -2 + ] + ], + [ + [ + /^[\s]/g, + 'sh_normal', + 2 + ], + [ + /(?=^[*]{3})/g, + 'sh_newfile', + -2 + ], + [ + /^diff/g, + 'sh_normal', + 2 + ] + ], + [ + [ + /^[\d]/g, + 'sh_difflines', + 2 + ], + [ + /^[<]/g, + 'sh_oldfile', + 2 + ], + [ + /^[>]/g, + 'sh_newfile', + 2 + ] + ] +]; diff --git a/static/sh_main.min.js b/static/sh_main.min.js new file mode 100644 index 00000000..31d1ba09 --- /dev/null +++ b/static/sh_main.min.js @@ -0,0 +1,4 @@ +/* Copyright (C) 2007, 2008 gnombat@users.sourceforge.net */ +/* License: http://shjs.sourceforge.net/doc/gplv3.html */ + +if(!this.sh_languages){this.sh_languages={}}var sh_requests={};function sh_isEmailAddress(a){if(/^mailto:/.test(a)){return false}return a.indexOf("@")!==-1}function sh_setHref(b,c,d){var a=d.substring(b[c-2].pos,b[c-1].pos);if(a.length>=2&&a.charAt(0)==="<"&&a.charAt(a.length-1)===">"){a=a.substr(1,a.length-2)}if(sh_isEmailAddress(a)){a="mailto:"+a}b[c-2].node.href=a}function sh_konquerorExec(b){var a=[""];a.index=b.length;a.input=b;return a}function sh_highlightString(B,o){if(/Konqueror/.test(navigator.userAgent)){if(!o.konquered){for(var F=0;FI){x(g.substring(I,E.index),null)}var e=O[u];var J=e[1];var b;if(J instanceof Array){for(var L=0;L0){var e=b.split(" ");for(var c=0;c0){a.push(e[c])}}}return a}function sh_addClass(c,a){var d=sh_getClasses(c);for(var b=0;b element with class="'+h+'", but no such language exists'}}break}}}}; \ No newline at end of file diff --git a/static/sh_ruby.min.js b/static/sh_ruby.min.js new file mode 100644 index 00000000..30928e60 --- /dev/null +++ b/static/sh_ruby.min.js @@ -0,0 +1 @@ +if(!this.sh_languages){this.sh_languages={}}sh_languages.ruby=[[[/\b(?:require)\b/g,"sh_preproc",-1],[/\b[+-]?(?:(?:0x[A-Fa-f0-9]+)|(?:(?:[\d]*\.)?[\d]+(?:[eE][+-]?[\d]+)?))u?(?:(?:int(?:8|16|32|64))|L)?\b/g,"sh_number",-1],[/"/g,"sh_string",1],[/'/g,"sh_string",2],[/|\|/g,"sh_symbol",-1],[/(#)(\{)/g,["sh_symbol","sh_cbracket"],-1],[/#/g,"sh_comment",5],[/\{|\}/g,"sh_cbracket",-1]],[[/$/g,null,-2],[/\\(?:\\|")/g,null,-1],[/"/g,"sh_string",-2]],[[/$/g,null,-2],[/\\(?:\\|')/g,null,-1],[/'/g,"sh_string",-2]],[[/$/g,null,-2],[/>/g,"sh_string",-2]],[[/^(?:\=end)/g,"sh_comment",-2]],[[/$/g,null,-2]]]; \ No newline at end of file diff --git a/static/sh_style.css b/static/sh_style.css new file mode 100644 index 00000000..6cd20b47 --- /dev/null +++ b/static/sh_style.css @@ -0,0 +1,66 @@ +pre.sh_sourceCode { + background-color: white; + color: black; + font-style: normal; + font-weight: normal; +} + +pre.sh_sourceCode .sh_keyword { color: blue; font-weight: bold; } /* language keywords */ +pre.sh_sourceCode .sh_type { color: darkgreen; } /* basic types */ +pre.sh_sourceCode .sh_usertype { color: teal; } /* user defined types */ +pre.sh_sourceCode .sh_string { color: red; font-family: monospace; } /* strings and chars */ +pre.sh_sourceCode .sh_regexp { color: orange; font-family: monospace; } /* regular expressions */ +pre.sh_sourceCode .sh_specialchar { color: pink; font-family: monospace; } /* e.g., \n, \t, \\ */ +pre.sh_sourceCode .sh_comment { color: brown; font-style: italic; } /* comments */ +pre.sh_sourceCode .sh_number { color: purple; } /* literal numbers */ +pre.sh_sourceCode .sh_preproc { color: darkblue; font-weight: bold; } /* e.g., #include, import */ +pre.sh_sourceCode .sh_symbol { color: darkred; } /* e.g., <, >, + */ +pre.sh_sourceCode .sh_function { color: black; font-weight: bold; } /* function calls and declarations */ +pre.sh_sourceCode .sh_cbracket { color: red; } /* block brackets (e.g., {, }) */ +pre.sh_sourceCode .sh_todo { font-weight: bold; background-color: cyan; } /* TODO and FIXME */ + +/* Predefined variables and functions (for instance glsl) */ +pre.sh_sourceCode .sh_predef_var { color: darkblue; } +pre.sh_sourceCode .sh_predef_func { color: darkblue; font-weight: bold; } + +/* for OOP */ +pre.sh_sourceCode .sh_classname { color: teal; } + +/* line numbers (not yet implemented) */ +pre.sh_sourceCode .sh_linenum { color: black; font-family: monospace; } + +/* Internet related */ +pre.sh_sourceCode .sh_url { color: blue; text-decoration: underline; font-family: monospace; } + +/* for ChangeLog and Log files */ +pre.sh_sourceCode .sh_date { color: blue; font-weight: bold; } +pre.sh_sourceCode .sh_time, pre.sh_sourceCode .sh_file { color: darkblue; font-weight: bold; } +pre.sh_sourceCode .sh_ip, pre.sh_sourceCode .sh_name { color: darkgreen; } + +/* for Prolog, Perl... */ +pre.sh_sourceCode .sh_variable { color: darkgreen; } + +/* for LaTeX */ +pre.sh_sourceCode .sh_italics { color: darkgreen; font-style: italic; } +pre.sh_sourceCode .sh_bold { color: darkgreen; font-weight: bold; } +pre.sh_sourceCode .sh_underline { color: darkgreen; text-decoration: underline; } +pre.sh_sourceCode .sh_fixed { color: green; font-family: monospace; } +pre.sh_sourceCode .sh_argument { color: darkgreen; } +pre.sh_sourceCode .sh_optionalargument { color: purple; } +pre.sh_sourceCode .sh_math { color: orange; } +pre.sh_sourceCode .sh_bibtex { color: blue; } + +/* for diffs */ +pre.sh_sourceCode .sh_oldfile { color: orange; } +pre.sh_sourceCode .sh_newfile { color: darkgreen; } +pre.sh_sourceCode .sh_difflines { color: blue; } + +/* for css */ +pre.sh_sourceCode .sh_selector { color: purple; } +pre.sh_sourceCode .sh_property { color: blue; } +pre.sh_sourceCode .sh_value { color: darkgreen; font-style: italic; } + +/* other */ +pre.sh_sourceCode .sh_section { color: black; font-weight: bold; } +pre.sh_sourceCode .sh_paren { color: red; } +pre.sh_sourceCode .sh_attribute { color: darkgreen; } diff --git a/static/state.html b/static/state.html index 0997e314..e79e31c2 100644 --- a/static/state.html +++ b/static/state.html @@ -11,7 +11,9 @@

  • Terraform version: {{details.terraform_version}}
  • Serial: {{details.serial}}
  • -
  • Version:
  • +
  • Version:
  • +
  • Compare with: +

diff --git a/static/terraboard.css b/static/terraboard.css index 44b256e1..aacee877 100644 --- a/static/terraboard.css +++ b/static/terraboard.css @@ -161,7 +161,7 @@ body { padding-top: 60px; } float: right; } pre.sh_sourceCode .sh_oldfile { - color: red; + color: red !important; } .resource-title { white-space: nowrap; diff --git a/static/terraboard.js b/static/terraboard.js index 417e5a42..35b712fe 100644 --- a/static/terraboard.js +++ b/static/terraboard.js @@ -4,6 +4,9 @@ var app = angular.module("terraboard", ['ngRoute', 'ngSanitize', 'ui.select', 'c $routeProvider.when("/", { templateUrl: "static/main.html", controller: "tbMainCtrl" + }).when("/state/compare/:path*", { + templateUrl: "static/compare.html", + controller: "tbCompareCtrl" }).when("/state/:path*", { templateUrl: "static/state.html", controller: "tbStateCtrl" @@ -199,6 +202,10 @@ app.controller("tbStateCtrl", ['$scope', '$http', '$location', function($scope, $scope.$watch('selectedVersion', function(ver) { $location.search('versionid', ver.versionId); }); + + $scope.$watch('compareVersion', function(ver) { + $location.url('state/compare/'+$scope.path+'?from='+$scope.selectedVersion.versionId+'&to='+ver.versionId); + }); }); $http.get('api'+$location.url(), {cache: true}).then(function(response){ @@ -256,6 +263,59 @@ app.controller("tbStateCtrl", ['$scope', '$http', '$location', function($scope, }); }]); +app.directive("hlcode", ['$timeout', function($timeout) { + return { + restrict: "E", + scope: { + code: '=code', + lang: '=lang' + }, + link: function() { + $timeout(sh_highlightDocument, 0, false); + }, + template: "
{{code}}
" + } +}]); + +app.controller("tbCompareCtrl", ['$scope', '$http', '$location', function($scope, $http, $location) { + $http.get('api'+$location.url()).then(function(response){ + $scope.compare = response.data; + + $scope.only_in_old = Object.keys($scope.compare.differences.only_in_old).length; + $scope.only_in_new = Object.keys($scope.compare.differences.only_in_new).length; + $scope.differences = Object.keys($scope.compare.differences.resource_diff).length; + }); + + $scope.fromVersion = { + versionId: $location.search().from + }; + + $scope.toVersion = { + versionId: $location.search().to + }; + + var key = $location.url().replace('/state/compare/', ''); + $http.get('api/state/activity/'+key).then(function(response){ + $scope.versions = []; + for (i=0; i