1/** 2 * Copyright (c) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you 5 * may not use this file except in compliance with the License. You may 6 * obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 * implied. See the License for the specific language governing 14 * permissions and limitations under the License. 15 */ 16 17(function($) { 18 19 var _isModalOpen = false; 20 var _isReadOnly = true; 21 var _allTestsSet = new Set(); 22 var _allBranches = []; 23 var _allDevices = []; 24 25 var _writableSummary = 'Known test failures are acknowledged below for specific branch and \ 26 device configurations, and corresponding test breakage alerts will be silenced. Click an \ 27 entry to edit or see more information about the test failure.' 28 var _readOnlySummary = 'Known test failures are acknowledged below for specific branch and \ 29 device configurations, and corresponding test breakage alerts will be silenced. Click an \ 30 entry to see more information about the test failure. To add, edit, or remove a test \ 31 acknowledgment, contact a VTS Dashboard administrator.' 32 33 $.widget('custom.sizedAutocomplete', $.ui.autocomplete, { 34 options: { 35 parent: '' 36 }, 37 _resizeMenu: function() { 38 this.menu.element.outerWidth($(this.options.parent).width()); 39 } 40 }); 41 42 /** 43 * Remove an acknowledgment from the list. 44 * @param ack (jQuery object) The object for acknowledgment. 45 * @param key (String) The value to display next to the label. 46 */ 47 function removeAcknowledgment(ack, key) { 48 if (ack.hasClass('disabled')) { 49 return; 50 } 51 ack.addClass('disabled'); 52 $.ajax({ 53 url: '/api/test_acknowledgments/' + key, 54 type: 'DELETE' 55 }).always(function() { 56 ack.removeClass('disabled'); 57 }).then(function() { 58 ack.slideUp(150, function() { 59 ack.remove(); 60 }); 61 }); 62 } 63 64 /** 65 * Callback for when a chip is removed from a chiplist. 66 * @param text (String) The value stored in the chip. 67 * @param allChipsSet (Set) The set of all chip values. 68 * @param allIndicator (jQuery object) The object for "All" indicator adjacent to the chips. 69 */ 70 function chipRemoveCallback(text, allChipsSet, allIndicator) { 71 allChipsSet.delete(text); 72 if (allChipsSet.size == 0) { 73 allIndicator.show(); 74 } 75 } 76 77 /** 78 * Add chips to the chip UI. 79 * @param allChipsSet (Set) The set of all chip values. 80 * @param container (jQuery object) The object in which to insert the chips. 81 * @param chipList (list) The list of chip values to insert. 82 * @param allIndicator (jQuery object) The object for "All" indicator adjacent to the chips. 83 */ 84 function addChips(allChipsSet, container, chipList, allIndicator) { 85 if (chipList && chipList.length > 0) { 86 chipList.forEach(function(text) { 87 if (allChipsSet.has(text)) return; 88 var chip = $('<span class="chip">' + text + '</span>'); 89 if (!_isReadOnly) { 90 var icon = $('<i class="material-icons">clear</i>').appendTo(chip); 91 icon.click(function() { 92 chipRemoveCallback(text, allChipsSet, allIndicator); 93 }); 94 } 95 chip.appendTo(container); 96 allChipsSet.add(text); 97 }); 98 allIndicator.hide(); 99 } 100 } 101 102 /** 103 * Create a chip input UI. 104 * @param container (jQuery object) The object in which to insert the input box. 105 * @param placeholder (String) The placeholder text to display in the input. 106 * @param allChipsSet (Set) The set of all chip values. 107 * @param chipContainer (jQuery object) The object in which to insert new chips from the input. 108 * @param allIndicator (jQuery object) The object for "All" indicator adjacent to the chips. 109 * @returns The chip input jQuery object. 110 */ 111 function addChipInput(container, placeholder, allChipsSet, chipContainer, allIndicator) { 112 var input = $('<input type="text"></input>'); 113 input.attr('placeholder', placeholder); 114 input.keyup(function(e) { 115 if (e.keyCode === 13 && input.val().trim()) { 116 addChips(allChipsSet, chipContainer, [input.val()], allIndicator); 117 input.val(''); 118 } 119 }); 120 var addButton = $('<i class="material-icons add-button">add</i>'); 121 addButton.click(function() { 122 if (input.val().trim()) { 123 addChips(allChipsSet, chipContainer, [input.val()], allIndicator); 124 input.val(''); 125 addButton.hide(); 126 } 127 }); 128 addButton.hide(); 129 input.focus(function() { 130 addButton.show(); 131 }); 132 input.focusout(function() { 133 if (!input.val().trim()) { 134 addButton.hide(); 135 } 136 }); 137 var holder = $('<div class="col s12 input-container"></div>').appendTo(container); 138 input.appendTo(holder); 139 addButton.appendTo(holder); 140 return input; 141 } 142 143 /** 144 * Callback to save changes to the acknowledgment. 145 * @param ack (jQuery object) The object for acknowledgment. 146 * @param modal (jQuery object) The jQueryUI modal object which invoked the callback. 147 * @param key (String) The key associated with the acknowledgment. 148 * @param test (String) The test name in the acknowledgment. 149 * @param branchSet (Set) The set of all branches in the acknowledgment. 150 * @param deviceSet (Set) The set of all devoces in the acknowledgment. 151 * @param testCaseSet (Set) The set of all test cases in the acknowledgment. 152 * @param note (String) The note in the acknowledgment. 153 */ 154 function saveCallback(ack, modal, key, test, branchSet, deviceSet, testCaseSet, note) { 155 var allEmpty = true; 156 var firstUnemptyInput = null; 157 var vals = modal.find('.modal-section>.input-container>input').each(function(_, input) { 158 if (!!$(input).val()) { 159 allEmpty = false; 160 if (!firstUnemptyInput) firstUnemptyInput = $(input); 161 } 162 }); 163 if (!allEmpty) { 164 firstUnemptyInput.focus(); 165 return false; 166 } 167 var branches = Array.from(branchSet); 168 branches.sort(); 169 var devices = Array.from(deviceSet); 170 devices.sort(); 171 var testCaseNames = Array.from(testCaseSet); 172 testCaseNames.sort(); 173 var data = { 174 'key' : key, 175 'testName' : test, 176 'branches' : branches, 177 'devices' : devices, 178 'testCaseNames' : testCaseNames, 179 'note': note 180 }; 181 $.post('/api/test_acknowledgments', JSON.stringify(data)).done(function(newKey) { 182 var newAck = createAcknowledgment(newKey, test, branches, devices, testCaseNames, note); 183 if (key == null) { 184 ack.replaceWith(newAck.hide()); 185 newAck.slideDown(150); 186 } else { 187 ack.replaceWith(newAck); 188 } 189 }).always(function() { 190 modal.modal({ 191 complete: function() { _isModalOpen = false; } 192 }); 193 modal.modal('close'); 194 }); 195 } 196 197 /** 198 * Callback to save changes to the acknowledgment. 199 * @param ack (jQuery object) The object for the acknowledgment. 200 * @param key (String) The key associated with the acknowledgment. 201 * @param test (String) The test name in the acknowledgment. 202 * @param branches (list) The list of all branches in the acknowledgment. 203 * @param devices (Set) The list of all devoces in the acknowledgment. 204 * @param testCases (Set) The list of all test cases in the acknowledgment. 205 * @param note (String) The note in the acknowledgment. 206 */ 207 function showModal(ack, key, test, branches, devices, testCases, note) { 208 if (_isModalOpen) { 209 return; 210 } 211 _isModalOpen = true; 212 var wrapper = $('#modal'); 213 wrapper.empty(); 214 wrapper.modal(); 215 var content = $('<div class="modal-content"><h4>Test Acknowledgment</h4></div>'); 216 var row = $('<div class="row"></div>').appendTo(content); 217 row.append('<div class="col s12"><h5><b>Test: </b>' + test + '</h5></div>'); 218 219 var branchSet = new Set(); 220 var branchContainer = $('<div class="col l4 s12 modal-section"></div>').appendTo(row); 221 var branchHeader = $('<h5></h5>').appendTo(branchContainer); 222 branchHeader.append('<b>Branches:</b>'); 223 var allBranchesLabel = $('<span> All</span>').appendTo(branchHeader); 224 var branchChips = $('<div class="col s12 chips branch-chips"></div>').appendTo(branchContainer); 225 addChips(branchSet, branchChips, branches, allBranchesLabel); 226 if (!_isReadOnly) { 227 var branchInput = addChipInput( 228 branchContainer, 'Specify a branch...', branchSet, branchChips, allBranchesLabel); 229 branchInput.sizedAutocomplete({ 230 source: _allBranches, 231 classes: { 232 'ui-autocomplete': 'card autocomplete-dropdown' 233 }, 234 parent: branchInput 235 }); 236 } 237 238 var deviceSet = new Set(); 239 var deviceContainer = $('<div class="col l4 s12 modal-section"></div>').appendTo(row); 240 var deviceHeader = $('<h5></h5>').appendTo(deviceContainer); 241 deviceHeader.append('<b>Devices:</b>'); 242 var allDevicesLabel = $('<span> All</span>').appendTo(deviceHeader); 243 var deviceChips = $('<div class="col s12 chips device-chips"></div>').appendTo(deviceContainer); 244 addChips(deviceSet, deviceChips, devices, allDevicesLabel); 245 if (!_isReadOnly) { 246 var deviceInput = addChipInput( 247 deviceContainer, 'Specify a device...', deviceSet, deviceChips, allDevicesLabel); 248 deviceInput.sizedAutocomplete({ 249 source: _allDevices, 250 classes: { 251 'ui-autocomplete': 'card autocomplete-dropdown' 252 }, 253 parent: deviceInput 254 }); 255 } 256 257 var testCaseSet = new Set(); 258 var testCaseContainer = $('<div class="col l4 s12 modal-section"></div>').appendTo(row); 259 var testCaseHeader = $('<h5></h5>').appendTo(testCaseContainer); 260 testCaseHeader.append('<b>Test Cases:</b>'); 261 var allTestCasesLabel = $('<span> All</span>').appendTo(testCaseHeader); 262 var testCaseChips = $('<div class="col s12 chips test-case-chips"></div>').appendTo( 263 testCaseContainer); 264 addChips(testCaseSet, testCaseChips, testCases, allTestCasesLabel); 265 var testCaseInput = null; 266 if (!_isReadOnly) { 267 testCaseInput = addChipInput( 268 testCaseContainer, 'Specify a test case...', testCaseSet, testCaseChips, allTestCasesLabel); 269 } 270 271 row.append('<div class="col s12"><h5><b>Note:</b></h5></div>'); 272 var inputField = $('<div class="input-field col s12"></div>').appendTo(row); 273 var textArea = $('<textarea placeholder="Type a note..."></textarea>'); 274 textArea.addClass('materialize-textarea note-field'); 275 textArea.appendTo(inputField); 276 textArea.val(note); 277 if (_isReadOnly) { 278 textArea.attr('disabled', true); 279 } 280 281 content.appendTo(wrapper); 282 var footer = $('<div class="modal-footer"></div>'); 283 if (!_isReadOnly) { 284 var save = $('<a class="btn">Save</a></div>').appendTo(footer); 285 save.click(function() { 286 saveCallback(ack, wrapper, key, test, branchSet, deviceSet, testCaseSet, textArea.val()); 287 }); 288 } 289 var close = $('<a class="btn-flat">Close</a></div>').appendTo(footer); 290 close.click(function() { 291 wrapper.modal({ 292 complete: function() { _isModalOpen = false; } 293 }); 294 wrapper.modal('close'); 295 }) 296 footer.appendTo(wrapper); 297 if (!_isReadOnly) { 298 $.get('/api/test_run?test=' + test + '×tamp=latest').done(function(data) { 299 var allTestCases = data.reduce(function(array, column) { 300 return array.concat(column.data); 301 }, []); 302 testCaseInput.sizedAutocomplete({ 303 source: allTestCases, 304 classes: { 305 'ui-autocomplete': 'card autocomplete-dropdown' 306 }, 307 parent: testCaseInput 308 }); 309 }).always(function() { 310 wrapper.modal('open'); 311 }); 312 } else { 313 wrapper.modal('open'); 314 } 315 } 316 317 /** 318 * Create a test acknowledgment object. 319 * @param key (String) The key associated with the acknowledgment. 320 * @param test (String) The test name in the acknowledgment. 321 * @param branches (list) The list of all branches in the acknowledgment. 322 * @param devices (Set) The list of all devoces in the acknowledgment. 323 * @param testCases (Set) The list of all test cases in the acknowledgment. 324 * @param note (String) The note in the acknowledgment. 325 */ 326 function createAcknowledgment(key, test, branches, devices, testCases, note) { 327 var wrapper = $('<div class="col s12 ack-entry"></div>'); 328 var details = $('<div class="col card hoverable"></div>').appendTo(wrapper); 329 details.addClass(_isReadOnly ? 's12' : 's11') 330 var testDiv = $('<div class="col s12"><b>' + test + '</b></div>').appendTo(details); 331 var infoBtn = $('<span class="info-icon right"></a>').appendTo(testDiv); 332 infoBtn.append('<i class="material-icons">info_outline</i>'); 333 details.click(function() { 334 showModal(wrapper, key, test, branches, devices, testCases, note); 335 }); 336 var branchesSummary = 'All'; 337 if (!!branches && branches.length == 1) { 338 branchesSummary = branches[0]; 339 } else if (!!branches && branches.length > 1) { 340 branchesSummary = branches[0]; 341 branchesSummary += '<span class="count-indicator"> (+' + (branches.length - 1) + ')</span>'; 342 } 343 $('<div class="col l4 s12"><b>Branches: </b>' + branchesSummary + '</div>').appendTo(details); 344 var devicesSummary = 'All'; 345 if (!!devices && devices.length == 1) { 346 devicesSummary = devices[0]; 347 } else if (!!devices && devices.length > 1) { 348 devicesSummary = devices[0]; 349 devicesSummary += '<span class="count-indicator"> (+' + (devices.length - 1) + ')</span>'; 350 } 351 $('<div class="col l4 s12"><b>Devices: </b>' + devicesSummary + '</div>').appendTo(details); 352 var testCaseSummary = 'All'; 353 if (!!testCases && testCases.length == 1) { 354 testCaseSummary = testCases[0]; 355 } else if (!!testCases && testCases.length > 1) { 356 testCaseSummary = testCases[0]; 357 testCaseSummary += '<span class="count-indicator"> (+' + (testCases.length - 1) + ')</span>'; 358 } 359 details.append('<div class="col l4 s12"><b>Test Cases: </b>' + testCaseSummary + '</div>'); 360 361 if (!_isReadOnly) { 362 var btnContainer = $('<div class="col s1 center btn-container"></div>'); 363 364 var clear = $('<a class="col s12 btn-flat remove-button"></a>'); 365 clear.append('<i class="material-icons">clear</i>'); 366 clear.attr('title', 'Remove'); 367 clear.click(function() { removeAcknowledgment(wrapper, key); }); 368 clear.appendTo(btnContainer); 369 370 btnContainer.appendTo(wrapper); 371 } 372 return wrapper; 373 } 374 375 /** 376 * Create a test acknowledgments UI. 377 * @param allTests (list) The list of all test names. 378 * @param allBranches (list) The list of all branches. 379 * @param allDevices (list) The list of all device names. 380 * @param testAcknowledgments (list) JSON-serialized TestAcknowledgmentEntity object list. 381 * @param readOnly (boolean) True if the acknowledgments are read-only, false if mutable. 382 */ 383 $.fn.testAcknowledgments = function( 384 allTests, allBranches, allDevices, testAcknowledgments, readOnly) { 385 var self = $(this); 386 _allTestsSet = new Set(allTests); 387 _allBranches = allBranches; 388 _allDevices = allDevices; 389 _isReadOnly = readOnly; 390 var searchRow = $('<div class="search-row"></div>'); 391 var headerRow = $('<div></div>'); 392 var acks = $('<div class="acknowledgments"></div>'); 393 394 if (!_isReadOnly) { 395 var inputWrapper = $('<div class="input-field col s8"></div>'); 396 var input = $('<input type="text"></input>').appendTo(inputWrapper); 397 inputWrapper.append('<label>Search for tests to add an acknowledgment</label>'); 398 inputWrapper.appendTo(searchRow); 399 input.sizedAutocomplete({ 400 source: allTests, 401 classes: { 402 'ui-autocomplete': 'card autocomplete-dropdown' 403 }, 404 parent: input 405 }); 406 407 var btnWrapper = $('<div class="col s1"></div>'); 408 var btn = $('<a class="btn waves-effect waves-light red btn-floating"></a>'); 409 btn.append('<i class="material-icons">add</a>'); 410 btn.appendTo(btnWrapper); 411 btnWrapper.appendTo(searchRow); 412 btn.click(function() { 413 if (!_allTestsSet.has(input.val())) return; 414 var ack = createAcknowledgment(undefined, input.val()); 415 ack.hide().prependTo(acks); 416 showModal(ack, undefined, input.val()); 417 }); 418 searchRow.appendTo(self); 419 } 420 421 var headerCol = $('<div class="col s12 section-header-col"></div>').appendTo(headerRow); 422 if (_isReadOnly) { 423 headerCol.append('<p class="acknowledgment-info">' + _readOnlySummary + '</p>'); 424 } else { 425 headerCol.append('<p class="acknowledgment-info">' + _writableSummary + '</p>'); 426 } 427 headerRow.appendTo(self); 428 429 testAcknowledgments.forEach(function(ack) { 430 var wrapper = createAcknowledgment( 431 ack.key, ack.testName, ack.branches, ack.devices, ack.testCaseNames, ack.note); 432 wrapper.appendTo(acks); 433 }); 434 acks.appendTo(self); 435 436 self.append('<div class="modal modal-fixed-footer acknowledgments-modal" id="modal"></div>'); 437 }; 438 439})(jQuery); 440