ソースを参照

AMBARI-18247 Capacity scheduler's dependent config suggestion is incomprehensible (zhewang)

Zhe (Joe) Wang 8 年 前
コミット
32af6a811b

+ 19 - 1
ambari-web/app/mixins/common/configs/config_recommendations.js

@@ -96,6 +96,18 @@ App.ConfigRecommendations = Em.Mixin.create({
     Em.assert('name and fileName should be defined', name && fileName);
     var site = App.config.getConfigTagFromFileName(fileName);
     var service = App.config.get('serviceByConfigTypeMap')[site];
+    var trimAndSort = function (value) {
+      if (value == null) {
+        return [];
+      }
+      var values = value.split("\n").filter(function (item) {
+        return item != "";
+      }).sort().join("\n");
+      return difflib.stringAsLines(values);
+    };
+    var initialValues = trimAndSort(initialValue);
+    var recommendedValues = trimAndSort(recommendedValue);
+
     var recommendation = {
       saveRecommended: true,
       saveRecommendedDefault: true,
@@ -112,7 +124,13 @@ App.ConfigRecommendations = Em.Mixin.create({
       allowChangeGroup: false,//TODO groupName!= "Default" && (service.get('serviceName') != this.get('selectedService.serviceName'))
       //TODO&& (App.ServiceConfigGroup.find().filterProperty('serviceName', service.get('serviceName')).length > 1), //TODO
       serviceDisplayName: service.get('displayName'),
-      recommendedValue: recommendedValue
+      recommendedValue: recommendedValue,
+
+      diff: new Handlebars.SafeString(diffview.buildView({
+        baseTextLines: initialValues,
+        newTextLines: recommendedValues,
+        opcodes: new difflib.SequenceMatcher(initialValues, recommendedValues).get_opcodes()
+      }).outerHTML)
     };
     this.get('recommendations').pushObject(recommendation);
     return recommendation;

+ 13 - 16
ambari-web/app/templates/common/modal_popups/dependent_configs_list.hbs

@@ -19,7 +19,7 @@
 <div class="alert alert-warning">
   {{t popup.dependent.configs.title}}
 </div>
-<div id="config-dependencies" class="limited-height-2">
+<span id="config-dependencies" class="limited-height-2">
   <table class="table table-striped">
     <thead>
     <tr>
@@ -28,8 +28,14 @@
       <th>{{t common.service}}</th>
       <th>{{t common.configGroup}}</th>
       <th>{{t common.fileName}}</th>
-      <th>{{t popup.dependent.configs.table.currentValue}}</th>
-      <th>{{t popup.dependent.configs.table.recommendedValue}}</th>
+      <th class="row-fliud">
+          <div class="span6">
+            {{t popup.dependent.configs.table.currentValue}}
+          </div>
+          <div class="span6">
+            {{t popup.dependent.configs.table.recommendedValue}}
+          </div>
+      </th>
     </tr>
     </thead>
     <tbody>
@@ -45,19 +51,10 @@
           </a></span>
         </td>
         <td class="config-dependency-filename">{{recommendation.propertyFileName}}</td>
-        <td class="config-dependency-value">
-          {{#if recommendation.notDefined}}
-            <i>{{t popup.dependent.configs.table.not.defined}}</i>
-          {{else}}
-            {{recommendation.initialValue}}
-          {{/if}}
-        </td>
-        <td class="config-dependency-recommended-value">
-          {{#if recommendation.isDeleted}}
-            <i>{{t common.removed}}</i>
-          {{else}}
-            {{recommendation.recommendedValue}}
-          {{/if}}
+        <td>
+          <div>
+            {{recommendation.diff}}
+          </div>
         </td>
       </tr>
     {{/each}}

+ 2 - 2
ambari-web/app/utils/config.js

@@ -752,8 +752,8 @@ App.config = Em.Object.create({
     return configs.sort(function(a, b) {
       if (Em.get(a, 'index') > Em.get(b, 'index')) return 1;
       if (Em.get(a, 'index') < Em.get(b, 'index')) return -1;
-      if (Em.get(a, 'name') > Em.get(b, 'index')) return 1;
-      if (Em.get(a, 'name') < Em.get(b, 'index')) return -1;
+      if (Em.get(a, 'name') > Em.get(b, 'name')) return 1;
+      if (Em.get(a, 'name') < Em.get(b, 'name')) return -1;
       return 0;
     });
   },

+ 3 - 0
ambari-web/config.coffee

@@ -60,6 +60,8 @@ exports.config =
           'vendor/scripts/jquery.sticky-kit.js',
           'vendor/scripts/underscore.js',
           'vendor/scripts/backbone.js',
+          'vendor/scripts/difflib.js',
+          'vendor/scripts/diffview.js',
           'vendor/scripts/visualsearch.js',
           'vendor/scripts/moment.js',
           'vendor/scripts/moment-timezone-with-data-2010-2020.js',
@@ -88,6 +90,7 @@ exports.config =
           'vendor/styles/bootstrap-checkbox.css',
           'vendor/styles/bootstrap-slider.min.css',
           'vendor/styles/bootstrap-switch.min.css',
+          'vendor/styles/diffview.css',
           'vendor/styles/visualsearch-datauri.css'
         ],
         after: ['app/styles/custom-ui.css']

+ 2 - 0
ambari-web/karma.conf.js

@@ -69,6 +69,8 @@ module.exports = function(config) {
       'vendor/scripts/jquery.ui.custom-effects.js',
       'vendor/scripts/jquery.timeago.js',
       'vendor/scripts/jquery.ajax-retry.js',
+      'vendor/scripts/difflib.js',
+      'vendor/scripts/diffview.js',
       'vendor/scripts/underscore.js',
       'vendor/scripts/backbone.js',
       'vendor/scripts/visualsearch.js',

+ 10 - 3
ambari-web/test/mixins/common/configs/config_recommendations_test.js

@@ -32,10 +32,12 @@ describe('App.ConfigRecommendations', function() {
       sinon.stub(App.config, 'get').withArgs('serviceByConfigTypeMap').returns({
         'pFile': Em.Object.create({serviceName: 'sName', displayName: 'sDisplayName'})
       });
+	  sinon.stub(Handlebars, 'SafeString');
     });
     afterEach(function() {
       instanceObject.formatParentProperties.restore();
       App.config.get.restore();
+	  Handlebars.SafeString.restore();
     });
 
     it('adds new recommendation', function() {
@@ -53,7 +55,8 @@ describe('App.ConfigRecommendations', function() {
         serviceName: 'sName',
         allowChangeGroup: false,
         serviceDisplayName: 'sDisplayName',
-        recommendedValue: 'pRecommended'
+        recommendedValue: 'pRecommended',
+		diff: {}
       });
       expect(instanceObject.getRecommendation('pName', 'pFile', 'pGroup')).to.eql(res);
     });
@@ -128,7 +131,8 @@ describe('App.ConfigRecommendations', function() {
 					serviceName: 'sName',
 					allowChangeGroup: false,
 					serviceDisplayName: 'sDisplayName',
-					recommendedValue: 'pRecommended'
+					recommendedValue: 'pRecommended',
+					diff: {}
 				}
 			},
 			{
@@ -148,7 +152,8 @@ describe('App.ConfigRecommendations', function() {
 					serviceName: 'sName',
 					allowChangeGroup: false,
 					serviceDisplayName: 'sDisplayName',
-					recommendedValue: undefined
+					recommendedValue: undefined,
+					diff: {}
 				}
 			}
 		];
@@ -160,11 +165,13 @@ describe('App.ConfigRecommendations', function() {
 					sinon.stub(App.config, 'get').withArgs('serviceByConfigTypeMap').returns({
 						'pFile': c.service
 					});
+					sinon.stub(Handlebars, 'SafeString');
 					recommendation = instanceObject.addRecommendation(c.name, c.file, c.group, c.recommended, c.initial, c.parent);
 				});
 
 				afterEach(function() {
 					App.config.get.restore();
+					Handlebars.SafeString.restore();
 				});
 
 				it(c.title, function() {

+ 413 - 0
ambari-web/vendor/scripts/difflib.js

@@ -0,0 +1,413 @@
+/***
+This is part of jsdifflib v1.0. <http://snowtide.com/jsdifflib>
+
+Copyright (c) 2007, Snowtide Informatics Systems, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright notice, this
+		list of conditions and the following disclaimer.
+	* Redistributions in binary form must reproduce the above copyright notice,
+		this list of conditions and the following disclaimer in the documentation
+		and/or other materials provided with the distribution.
+	* Neither the name of the Snowtide Informatics Systems nor the names of its
+		contributors may be used to endorse or promote products derived from this
+		software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+***/
+/* Author: Chas Emerick <cemerick@snowtide.com> */
+var __whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true};
+
+var difflib = {
+	defaultJunkFunction: function (c) {
+		return __whitespace.hasOwnProperty(c);
+	},
+	
+	stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); },
+	
+	stringAsLines: function (str) {
+		var lfpos = str.indexOf("\n");
+		var crpos = str.indexOf("\r");
+		var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r";
+		
+		var lines = str.split(linebreak);
+		for (var i = 0; i < lines.length; i++) {
+			lines[i] = difflib.stripLinebreaks(lines[i]);
+		}
+		
+		return lines;
+	},
+	
+	// iteration-based reduce implementation
+	__reduce: function (func, list, initial) {
+		if (initial != null) {
+			var value = initial;
+			var idx = 0;
+		} else if (list) {
+			var value = list[0];
+			var idx = 1;
+		} else {
+			return null;
+		}
+		
+		for (; idx < list.length; idx++) {
+			value = func(value, list[idx]);
+		}
+		
+		return value;
+	},
+	
+	// comparison function for sorting lists of numeric tuples
+	__ntuplecomp: function (a, b) {
+		var mlen = Math.max(a.length, b.length);
+		for (var i = 0; i < mlen; i++) {
+			if (a[i] < b[i]) return -1;
+			if (a[i] > b[i]) return 1;
+		}
+		
+		return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1);
+	},
+	
+	__calculate_ratio: function (matches, length) {
+		return length ? 2.0 * matches / length : 1.0;
+	},
+	
+	// returns a function that returns true if a key passed to the returned function
+	// is in the dict (js object) provided to this function; replaces being able to
+	// carry around dict.has_key in python...
+	__isindict: function (dict) {
+		return function (key) { return dict.hasOwnProperty(key); };
+	},
+	
+	// replacement for python's dict.get function -- need easy default values
+	__dictget: function (dict, key, defaultValue) {
+		return dict.hasOwnProperty(key) ? dict[key] : defaultValue;
+	},	
+	
+	SequenceMatcher: function (a, b, isjunk) {
+		this.set_seqs = function (a, b) {
+			this.set_seq1(a);
+			this.set_seq2(b);
+		}
+		
+		this.set_seq1 = function (a) {
+			if (a == this.a) return;
+			this.a = a;
+			this.matching_blocks = this.opcodes = null;
+		}
+		
+		this.set_seq2 = function (b) {
+			if (b == this.b) return;
+			this.b = b;
+			this.matching_blocks = this.opcodes = this.fullbcount = null;
+			this.__chain_b();
+		}
+		
+		this.__chain_b = function () {
+			var b = this.b;
+			var n = b.length;
+			var b2j = this.b2j = {};
+			var populardict = {};
+			for (var i = 0; i < b.length; i++) {
+				var elt = b[i];
+				if (b2j.hasOwnProperty(elt)) {
+					var indices = b2j[elt];
+					if (n >= 200 && indices.length * 100 > n) {
+						populardict[elt] = 1;
+						delete b2j[elt];
+					} else {
+						indices.push(i);
+					}
+				} else {
+					b2j[elt] = [i];
+				}
+			}
+	
+			for (var elt in populardict) {
+				if (populardict.hasOwnProperty(elt)) {
+					delete b2j[elt];
+				}
+			}
+			
+			var isjunk = this.isjunk;
+			var junkdict = {};
+			if (isjunk) {
+				for (var elt in populardict) {
+					if (populardict.hasOwnProperty(elt) && isjunk(elt)) {
+						junkdict[elt] = 1;
+						delete populardict[elt];
+					}
+				}
+				for (var elt in b2j) {
+					if (b2j.hasOwnProperty(elt) && isjunk(elt)) {
+						junkdict[elt] = 1;
+						delete b2j[elt];
+					}
+				}
+			}
+	
+			this.isbjunk = difflib.__isindict(junkdict);
+			this.isbpopular = difflib.__isindict(populardict);
+		}
+		
+		this.find_longest_match = function (alo, ahi, blo, bhi) {
+			var a = this.a;
+			var b = this.b;
+			var b2j = this.b2j;
+			var isbjunk = this.isbjunk;
+			var besti = alo;
+			var bestj = blo;
+			var bestsize = 0;
+			var j = null;
+			var k;
+	
+			var j2len = {};
+			var nothing = [];
+			for (var i = alo; i < ahi; i++) {
+				var newj2len = {};
+				var jdict = difflib.__dictget(b2j, a[i], nothing);
+				for (var jkey in jdict) {
+					if (jdict.hasOwnProperty(jkey)) {
+						j = jdict[jkey];
+						if (j < blo) continue;
+						if (j >= bhi) break;
+						newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1;
+						if (k > bestsize) {
+							besti = i - k + 1;
+							bestj = j - k + 1;
+							bestsize = k;
+						}
+					}
+				}
+				j2len = newj2len;
+			}
+	
+			while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) {
+				besti--;
+				bestj--;
+				bestsize++;
+			}
+				
+			while (besti + bestsize < ahi && bestj + bestsize < bhi &&
+					!isbjunk(b[bestj + bestsize]) &&
+					a[besti + bestsize] == b[bestj + bestsize]) {
+				bestsize++;
+			}
+	
+			while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) {
+				besti--;
+				bestj--;
+				bestsize++;
+			}
+			
+			while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) &&
+					a[besti + bestsize] == b[bestj + bestsize]) {
+				bestsize++;
+			}
+	
+			return [besti, bestj, bestsize];
+		}
+		
+		this.get_matching_blocks = function () {
+			if (this.matching_blocks != null) return this.matching_blocks;
+			var la = this.a.length;
+			var lb = this.b.length;
+	
+			var queue = [[0, la, 0, lb]];
+			var matching_blocks = [];
+			var alo, ahi, blo, bhi, qi, i, j, k, x;
+			while (queue.length) {
+				qi = queue.pop();
+				alo = qi[0];
+				ahi = qi[1];
+				blo = qi[2];
+				bhi = qi[3];
+				x = this.find_longest_match(alo, ahi, blo, bhi);
+				i = x[0];
+				j = x[1];
+				k = x[2];
+	
+				if (k) {
+					matching_blocks.push(x);
+					if (alo < i && blo < j)
+						queue.push([alo, i, blo, j]);
+					if (i+k < ahi && j+k < bhi)
+						queue.push([i + k, ahi, j + k, bhi]);
+				}
+			}
+			
+			matching_blocks.sort(difflib.__ntuplecomp);
+	
+			var i1 = 0, j1 = 0, k1 = 0, block = 0;
+			var i2, j2, k2;
+			var non_adjacent = [];
+			for (var idx in matching_blocks) {
+				if (matching_blocks.hasOwnProperty(idx)) {
+					block = matching_blocks[idx];
+					i2 = block[0];
+					j2 = block[1];
+					k2 = block[2];
+					if (i1 + k1 == i2 && j1 + k1 == j2) {
+						k1 += k2;
+					} else {
+						if (k1) non_adjacent.push([i1, j1, k1]);
+						i1 = i2;
+						j1 = j2;
+						k1 = k2;
+					}
+				}
+			}
+			
+			if (k1) non_adjacent.push([i1, j1, k1]);
+	
+			non_adjacent.push([la, lb, 0]);
+			this.matching_blocks = non_adjacent;
+			return this.matching_blocks;
+		}
+		
+		this.get_opcodes = function () {
+			if (this.opcodes != null) return this.opcodes;
+			var i = 0;
+			var j = 0;
+			var answer = [];
+			this.opcodes = answer;
+			var block, ai, bj, size, tag;
+			var blocks = this.get_matching_blocks();
+			for (var idx in blocks) {
+				if (blocks.hasOwnProperty(idx)) {
+					block = blocks[idx];
+					ai = block[0];
+					bj = block[1];
+					size = block[2];
+					tag = '';
+					if (i < ai && j < bj) {
+						tag = 'replace';
+					} else if (i < ai) {
+						tag = 'delete';
+					} else if (j < bj) {
+						tag = 'insert';
+					}
+					if (tag) answer.push([tag, i, ai, j, bj]);
+					i = ai + size;
+					j = bj + size;
+					
+					if (size) answer.push(['equal', ai, i, bj, j]);
+				}
+			}
+			
+			return answer;
+		}
+		
+		// this is a generator function in the python lib, which of course is not supported in javascript
+		// the reimplementation builds up the grouped opcodes into a list in their entirety and returns that.
+		this.get_grouped_opcodes = function (n) {
+			if (!n) n = 3;
+			var codes = this.get_opcodes();
+			if (!codes) codes = [["equal", 0, 1, 0, 1]];
+			var code, tag, i1, i2, j1, j2;
+			if (codes[0][0] == 'equal') {
+				code = codes[0];
+				tag = code[0];
+				i1 = code[1];
+				i2 = code[2];
+				j1 = code[3];
+				j2 = code[4];
+				codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2];
+			}
+			if (codes[codes.length - 1][0] == 'equal') {
+				code = codes[codes.length - 1];
+				tag = code[0];
+				i1 = code[1];
+				i2 = code[2];
+				j1 = code[3];
+				j2 = code[4];
+				codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)];
+			}
+	
+			var nn = n + n;
+			var group = [];
+			var groups = [];
+			for (var idx in codes) {
+				if (codes.hasOwnProperty(idx)) {
+					code = codes[idx];
+					tag = code[0];
+					i1 = code[1];
+					i2 = code[2];
+					j1 = code[3];
+					j2 = code[4];
+					if (tag == 'equal' && i2 - i1 > nn) {
+						group.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]);
+						groups.push(group);
+						group = [];
+						i1 = Math.max(i1, i2-n);
+						j1 = Math.max(j1, j2-n);
+					}
+					
+					group.push([tag, i1, i2, j1, j2]);
+				}
+			}
+			
+			if (group && !(group.length == 1 && group[0][0] == 'equal')) groups.push(group)
+			
+			return groups;
+		}
+		
+		this.ratio = function () {
+			matches = difflib.__reduce(
+							function (sum, triple) { return sum + triple[triple.length - 1]; },
+							this.get_matching_blocks(), 0);
+			return difflib.__calculate_ratio(matches, this.a.length + this.b.length);
+		}
+		
+		this.quick_ratio = function () {
+			var fullbcount, elt;
+			if (this.fullbcount == null) {
+				this.fullbcount = fullbcount = {};
+				for (var i = 0; i < this.b.length; i++) {
+					elt = this.b[i];
+					fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1;
+				}
+			}
+			fullbcount = this.fullbcount;
+	
+			var avail = {};
+			var availhas = difflib.__isindict(avail);
+			var matches = numb = 0;
+			for (var i = 0; i < this.a.length; i++) {
+				elt = this.a[i];
+				if (availhas(elt)) {
+					numb = avail[elt];
+				} else {
+					numb = difflib.__dictget(fullbcount, elt, 0);
+				}
+				avail[elt] = numb - 1;
+				if (numb > 0) matches++;
+			}
+			
+			return difflib.__calculate_ratio(matches, this.a.length + this.b.length);
+		}
+		
+		this.real_quick_ratio = function () {
+			var la = this.a.length;
+			var lb = this.b.length;
+			return _calculate_ratio(Math.min(la, lb), la + lb);
+		}
+		
+		this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction;
+		this.a = this.b = null;
+		this.set_seqs(a, b);
+	}
+};
+

+ 172 - 0
ambari-web/vendor/scripts/diffview.js

@@ -0,0 +1,172 @@
+/*
+This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
+
+Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright notice, this list
+      of conditions and the following disclaimer in the documentation and/or other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those of the
+authors and should not be interpreted as representing official policies, either expressed
+or implied, of Chas Emerick.
+*/
+diffview = {
+	/**
+	 * Builds and returns a visual diff view.  The single parameter, `params', should contain
+	 * the following values:
+	 *
+	 * - baseTextLines: the array of strings that was used as the base text input to SequenceMatcher
+	 * - newTextLines: the array of strings that was used as the new text input to SequenceMatcher
+	 * - opcodes: the array of arrays returned by SequenceMatcher.get_opcodes()
+	 * - baseTextName: the title to be displayed above the base text listing in the diff view; defaults
+	 *	   to "Base Text"
+	 * - newTextName: the title to be displayed above the new text listing in the diff view; defaults
+	 *	   to "New Text"
+	 * - contextSize: the number of lines of context to show around differences; by default, all lines
+	 *	   are shown
+	 * - viewType: if 0, a side-by-side diff view is generated (default); if 1, an inline diff view is
+	 *	   generated
+	 */
+	buildView: function (params) {
+		var baseTextLines = params.baseTextLines;
+		var newTextLines = params.newTextLines;
+		var opcodes = params.opcodes;
+		var contextSize = params.contextSize;
+		var inline = (params.viewType == 0 || params.viewType == 1) ? params.viewType : 0;
+
+		if (baseTextLines == null)
+			throw "Cannot build diff view; baseTextLines is not defined.";
+		if (newTextLines == null)
+			throw "Cannot build diff view; newTextLines is not defined.";
+		if (!opcodes)
+			throw "Canno build diff view; opcodes is not defined.";
+		
+		function celt (name, clazz) {
+			var e = document.createElement(name);
+			e.className = clazz;
+			return e;
+		}
+		
+		function telt (name, text) {
+			var e = document.createElement(name);
+			e.appendChild(document.createTextNode(text));
+			return e;
+		}
+		
+		function ctelt (name, clazz, text) {
+			var e = document.createElement(name);
+			e.className = clazz;
+			e.appendChild(document.createTextNode(text));
+			return e;
+		}
+	
+		var tdata = [];
+		
+		var rows = [];
+		var node2;
+		
+		/**
+		 * Adds two cells to the given row; if the given row corresponds to a real
+		 * line number (based on the line index tidx and the endpoint of the 
+		 * range in question tend), then the cells will contain the line number
+		 * and the line of text from textLines at position tidx (with the class of
+		 * the second cell set to the name of the change represented), and tidx + 1 will
+		 * be returned.	 Otherwise, tidx is returned, and two empty cells are added
+		 * to the given row.
+		 */
+		function addCells (row, tidx, tend, textLines, change) {
+			if (tidx < tend) {
+				row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
+				return tidx + 1;
+			} else {
+				row.appendChild(celt("td", "empty"));
+				return tidx;
+			}
+		}
+		
+		function addCellsInline (row, tidx, tidx2, textLines, change) {
+			row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
+		}
+		
+		for (var idx = 0; idx < opcodes.length; idx++) {
+			code = opcodes[idx];
+			change = code[0];
+			var b = code[1];
+			var be = code[2];
+			var n = code[3];
+			var ne = code[4];
+			var rowcnt = Math.max(be - b, ne - n);
+			var toprows = [];
+			var botrows = [];
+			for (var i = 0; i < rowcnt; i++) {
+				// jump ahead if we've alredy provided leading context or if this is the first range
+				if (contextSize && opcodes.length > 1 && ((idx > 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") {
+					var jump = rowcnt - ((idx == 0 ? 1 : 2) * contextSize);
+					if (jump > 1) {
+						toprows.push(node = celt("tr", "row"));
+						
+						b += jump;
+						n += jump;
+						i += jump - 1;
+						if (!inline) node.appendChild(ctelt("td", "skip span6", ""));
+						node.appendChild(ctelt("td", "skip span6", ""));
+						
+						// skip last lines if they're all equal
+						if (idx + 1 == opcodes.length) {
+							break;
+						} else {
+							continue;
+						}
+					}
+				}
+				
+				toprows.push(node = celt("tr", "row"));
+				if (inline) {
+					if (change == "insert") {
+						addCellsInline(node, null, n++, newTextLines, change + " span6");
+					} else if (change == "replace") {
+						botrows.push(node2 = celt("tr", "row"));
+						if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete span6");
+						if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert span6");
+					} else if (change == "delete") {
+						addCellsInline(node, b++, null, baseTextLines, change + " span6");
+					} else {
+						// equal
+						addCellsInline(node, b++, n++, baseTextLines, change + " span6");
+					}
+				} else {
+					b = addCells(node, b, be, baseTextLines, change + " span6");
+					n = addCells(node, n, ne, newTextLines, change + " span6");
+				}
+			}
+
+			for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]);
+			for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]);
+		}
+
+		tdata.push(node = document.createElement("tbody"));
+		for (var idx in rows) rows.hasOwnProperty(idx) && node.appendChild(rows[idx]);
+		
+		node = celt("table", "diff" + (inline ? " inlinediff" : ""));
+		for (var idx in tdata) tdata.hasOwnProperty(idx) && node.appendChild(tdata[idx]);
+		return node;
+	}
+};
+

+ 84 - 0
ambari-web/vendor/styles/diffview.css

@@ -0,0 +1,84 @@
+/*
+This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
+
+Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright notice, this list
+      of conditions and the following disclaimer in the documentation and/or other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those of the
+authors and should not be interpreted as representing official policies, either expressed
+or implied, of Chas Emerick.
+*/
+table.diff {
+	border-collapse:collapse;
+	border:1px solid darkgray;
+	white-space:pre-wrap
+}
+table.diff tbody { 
+	font-family:Courier, monospace
+}
+table.diff tbody th {
+	font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
+	background:#EED;
+	font-size:11px;
+	font-weight:normal;
+	border:1px solid #BBC;
+	color:#886;
+	padding:.3em .5em .1em 2em;
+	text-align:right;
+	vertical-align:top
+}
+table.diff thead {
+	border-bottom:1px solid #BBC;
+	background:#EFEFEF;
+	font-family:Verdana
+}
+table.diff thead th.texttitle {
+	text-align:left
+}
+table.diff tbody td {
+	padding:0px .4em;
+	padding-top:.4em;
+	vertical-align:top;
+	border:1px solid #BBC;
+}
+table.diff .empty {
+	background-color:#DDD !important;
+}
+table.diff .replace {
+	background-color:#FD8 !important;
+}
+table.diff .delete {
+	background-color:#E99 !important;
+}
+table.diff .skip {
+	background-color:#EFEFEF !important;
+	border:1px solid #AAA;
+	border-right:1px solid #BBC;
+}
+table.diff .insert {
+	background-color:#9E9 !important;
+}
+table.diff th.author {
+	text-align:right;
+	border-top:1px solid #BBC;
+	background:#EFEFEF
+}