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 + '&timestamp=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