blob: 737b018149558c12066ef8b1b071619c94364f09 [file] [log] [blame]
Monty Taylor4a781a72017-07-25 07:28:04 -04001/* global Image, jQuery */
2// jquery plugin for Zuul status page
3//
4// @licstart The following is the entire license notice for the
5// JavaScript code in this page.
6//
7// Copyright 2012 OpenStack Foundation
8// Copyright 2013 Timo Tijhof
9// Copyright 2013 Wikimedia Foundation
10// Copyright 2014 Rackspace Australia
11//
12// Licensed under the Apache License, Version 2.0 (the "License"); you may
13// not use this file except in compliance with the License. You may obtain
14// a copy of the License at
15//
16// http://www.apache.org/licenses/LICENSE-2.0
17//
18// Unless required by applicable law or agreed to in writing, software
19// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
20// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
21// License for the specific language governing permissions and limitations
22// under the License.
23//
24// @licend The above is the entire license notice
25// for the JavaScript code in this page.
26
27import RedImage from './images/red.png'
28import GreyImage from './images/grey.png'
29import GreenImage from './images/green.png'
30import BlackImage from './images/black.png'
31import LineImage from './images/line.png'
32import LineAngleImage from './images/line-angle.png'
33import LineTImage from './images/line-t.png';
34
35(function ($) {
36 function setCookie (name, value) {
37 document.cookie = name + '=' + value + '; path=/'
38 }
39
40 function readCookie (name, defaultValue) {
41 let nameEQ = name + '='
42 let ca = document.cookie.split(';')
43 for (let i = 0; i < ca.length; i++) {
44 let c = ca[i]
45 while (c.charAt(0) === ' ') {
46 c = c.substring(1, c.length)
47 }
48 if (c.indexOf(nameEQ) === 0) {
49 return c.substring(nameEQ.length, c.length)
50 }
51 }
52 return defaultValue
53 }
54
55 $.zuul = function (options) {
56 options = $.extend({
57 'enabled': true,
58 'graphite_url': '',
59 'source': 'status',
60 'source_data': null,
61 'msg_id': '#zuul_msg',
62 'pipelines_id': '#zuul_pipelines',
63 'queue_events_num': '#zuul_queue_events_num',
64 'queue_management_events_num': '#zuul_queue_management_events_num',
65 'queue_results_num': '#zuul_queue_results_num'
66 }, options)
67
68 let collapsedExceptions = []
69 let currentFilter = readCookie('zuul_filter_string', '')
70 let changeSetInURL = window.location.href.split('#')[1]
71 if (changeSetInURL) {
72 currentFilter = changeSetInURL
73 }
74 let $jq
75
76 let xhr
77 let zuulGraphUpdateCount = 0
78 let zuulSparklineURLs = {}
79
80 function getSparklineURL (pipelineName) {
81 if (options.graphite_url !== '') {
82 if (!(pipelineName in zuulSparklineURLs)) {
83 zuulSparklineURLs[pipelineName] = $.fn.graphite
84 .geturl({
85 url: options.graphite_url,
86 from: '-8hours',
87 width: 100,
88 height: 26,
89 margin: 0,
90 hideLegend: true,
91 hideAxes: true,
92 hideGrid: true,
93 target: [
94 'color(stats.gauges.zuul.pipeline.' + pipelineName +
95 ".current_changes, '6b8182')"
96 ]
97 })
98 }
99 return zuulSparklineURLs[pipelineName]
100 }
101 return false
102 }
103
104 let format = {
105 job: function (job) {
106 let $jobLine = $('<span />')
107
108 if (job.result !== null) {
109 $jobLine.append(
110 $('<a />')
111 .addClass('zuul-job-name')
112 .attr('href', job.report_url)
113 .text(job.name)
114 )
115 } else if (job.url !== null) {
116 $jobLine.append(
117 $('<a />')
118 .addClass('zuul-job-name')
119 .attr('href', job.url)
120 .text(job.name)
121 )
122 } else {
123 $jobLine.append(
124 $('<span />')
125 .addClass('zuul-job-name')
126 .text(job.name)
127 )
128 }
129
130 $jobLine.append(this.job_status(job))
131
132 if (job.voting === false) {
133 $jobLine.append(
134 $(' <small />')
135 .addClass('zuul-non-voting-desc')
136 .text(' (non-voting)')
137 )
138 }
139
140 $jobLine.append($('<div style="clear: both"></div>'))
141 return $jobLine
142 },
143
144 job_status: function (job) {
145 let result = job.result ? job.result.toLowerCase() : null
146 if (result === null) {
147 result = job.url ? 'in progress' : 'queued'
148 }
149
150 if (result === 'in progress') {
151 return this.job_progress_bar(job.elapsed_time,
152 job.remaining_time)
153 } else {
154 return this.status_label(result)
155 }
156 },
157
158 status_label: function (result) {
159 let $status = $('<span />')
160 $status.addClass('zuul-job-result label')
161
162 switch (result) {
163 case 'success':
164 $status.addClass('label-success')
165 break
166 case 'failure':
167 $status.addClass('label-danger')
168 break
169 case 'unstable':
170 $status.addClass('label-warning')
171 break
172 case 'skipped':
173 $status.addClass('label-info')
174 break
175 // 'in progress' 'queued' 'lost' 'aborted' ...
176 default:
177 $status.addClass('label-default')
178 }
179 $status.text(result)
180 return $status
181 },
182
183 job_progress_bar: function (elapsedTime, remainingTime) {
184 let progressPercent = 100 * (elapsedTime / (elapsedTime +
185 remainingTime))
186 let $barInner = $('<div />')
187 .addClass('progress-bar')
188 .attr('role', 'progressbar')
189 .attr('aria-valuenow', 'progressbar')
190 .attr('aria-valuemin', progressPercent)
191 .attr('aria-valuemin', '0')
192 .attr('aria-valuemax', '100')
193 .css('width', progressPercent + '%')
194
195 let $barOutter = $('<div />')
196 .addClass('progress zuul-job-result')
197 .append($barInner)
198
199 return $barOutter
200 },
201
202 enqueueTime: function (ms) {
203 // Special format case for enqueue time to add style
204 let hours = 60 * 60 * 1000
205 let now = Date.now()
206 let delta = now - ms
207 let status = 'text-success'
208 let text = this.time(delta, true)
209 if (delta > (4 * hours)) {
210 status = 'text-danger'
211 } else if (delta > (2 * hours)) {
212 status = 'text-warning'
213 }
214 return '<span class="' + status + '">' + text + '</span>'
215 },
216
217 time: function (ms, words) {
218 if (typeof (words) === 'undefined') {
219 words = false
220 }
221 let seconds = (+ms) / 1000
222 let minutes = Math.floor(seconds / 60)
223 let hours = Math.floor(minutes / 60)
224 seconds = Math.floor(seconds % 60)
225 minutes = Math.floor(minutes % 60)
226 let r = ''
227 if (words) {
228 if (hours) {
229 r += hours
230 r += ' hr '
231 }
232 r += minutes + ' min'
233 } else {
234 if (hours < 10) {
235 r += '0'
236 }
237 r += hours + ':'
238 if (minutes < 10) {
239 r += '0'
240 }
241 r += minutes + ':'
242 if (seconds < 10) {
243 r += '0'
244 }
245 r += seconds
246 }
247 return r
248 },
249
250 changeTotalProgressBar: function (change) {
251 let jobPercent = Math.floor(100 / change.jobs.length)
252 let $barOutter = $('<div />')
253 .addClass('progress zuul-change-total-result')
254
255 $.each(change.jobs, function (i, job) {
256 let result = job.result ? job.result.toLowerCase() : null
257 if (result === null) {
258 result = job.url ? 'in progress' : 'queued'
259 }
260
261 if (result !== 'queued') {
262 let $barInner = $('<div />')
263 .addClass('progress-bar')
264
265 switch (result) {
266 case 'success':
267 $barInner.addClass('progress-bar-success')
268 break
269 case 'lost':
270 case 'failure':
271 $barInner.addClass('progress-bar-danger')
272 break
273 case 'unstable':
274 $barInner.addClass('progress-bar-warning')
275 break
276 case 'in progress':
277 case 'queued':
278 break
279 }
280 $barInner.attr('title', job.name)
281 .css('width', jobPercent + '%')
282 $barOutter.append($barInner)
283 }
284 })
285 return $barOutter
286 },
287
288 changeHeader: function (change) {
289 let changeId = change.id || 'NA'
290
291 let $changeLink = $('<small />')
292 if (change.url !== null) {
293 let githubId = changeId.match(/^([0-9]+),([0-9a-f]{40})$/)
294 if (githubId) {
295 $changeLink.append(
296 $('<a />').attr('href', change.url).append(
297 $('<abbr />')
298 .attr('title', changeId)
299 .text('#' + githubId[1])
300 )
301 )
302 } else if (/^[0-9a-f]{40}$/.test(changeId)) {
303 let changeIdShort = changeId.slice(0, 7)
304 $changeLink.append(
305 $('<a />').attr('href', change.url).append(
306 $('<abbr />')
307 .attr('title', changeId)
308 .text(changeIdShort)
309 )
310 )
311 } else {
312 $changeLink.append(
313 $('<a />').attr('href', change.url).text(changeId)
314 )
315 }
316 } else {
317 if (changeId.length === 40) {
318 changeId = changeId.substr(0, 7)
319 }
320 $changeLink.text(changeId)
321 }
322
323 let $changeProgressRowLeft = $('<div />')
324 .addClass('col-xs-4')
325 .append($changeLink)
326 let $changeProgressRowRight = $('<div />')
327 .addClass('col-xs-8')
328 .append(this.changeTotalProgressBar(change))
329
330 let $changeProgressRow = $('<div />')
331 .addClass('row')
332 .append($changeProgressRowLeft)
333 .append($changeProgressRowRight)
334
335 let $projectSpan = $('<span />')
336 .addClass('change_project')
337 .text(change.project)
338
339 let $left = $('<div />')
340 .addClass('col-xs-8')
341 .append($projectSpan, $changeProgressRow)
342
343 let remainingTime = this.time(change.remaining_time, true)
344 let enqueueTime = this.enqueueTime(change.enqueue_time)
345 let $remainingTime = $('<small />').addClass('time')
346 .attr('title', 'Remaining Time').html(remainingTime)
347 let $enqueueTime = $('<small />').addClass('time')
348 .attr('title', 'Elapsed Time').html(enqueueTime)
349
350 let $right = $('<div />')
351 if (change.live === true) {
352 $right.addClass('col-xs-4 text-right')
353 .append($remainingTime, $('<br />'), $enqueueTime)
354 }
355
356 let $header = $('<div />')
357 .addClass('row')
358 .append($left, $right)
359 return $header
360 },
361
362 change_list: function (jobs) {
363 let format = this
364 let $list = $('<ul />')
365 .addClass('list-group zuul-patchset-body')
366
367 $.each(jobs, function (i, job) {
368 let $item = $('<li />')
369 .addClass('list-group-item')
370 .addClass('zuul-change-job')
371 .append(format.job(job))
372 $list.append($item)
373 })
374
375 return $list
376 },
377
378 changePanel: function (change) {
379 let $header = $('<div />')
380 .addClass('panel-heading zuul-patchset-header')
381 .append(this.changeHeader(change))
382
383 let panelId = change.id ? change.id.replace(',', '_')
384 : change.project.replace('/', '_') +
385 '-' + change.enqueue_time
386 let $panel = $('<div />')
387 .attr('id', panelId)
388 .addClass('panel panel-default zuul-change')
389 .append($header)
390 .append(this.change_list(change.jobs))
391
392 $header.click(this.toggle_patchset)
393 return $panel
394 },
395
396 change_status_icon: function (change) {
397 let iconFile = GreenImage
398 let iconTitle = 'Succeeding'
399
400 if (change.active !== true) {
401 // Grey icon
402 iconFile = GreyImage
403 iconTitle = 'Waiting until closer to head of queue to' +
404 ' start jobs'
405 } else if (change.live !== true) {
406 // Grey icon
407 iconFile = GreyImage
408 iconTitle = 'Dependent change required for testing'
409 } else if (change.failing_reasons &&
410 change.failing_reasons.length > 0) {
411 let reason = change.failing_reasons.join(', ')
412 iconTitle = 'Failing because ' + reason
413 if (reason.match(/merge conflict/)) {
414 // Black icon
415 iconFile = BlackImage
416 } else {
417 // Red icon
418 iconFile = RedImage
419 }
420 }
421
422 let $icon = $('<img />')
423 .attr('src', iconFile)
424 .attr('title', iconTitle)
425 .css('margin-top', '-6px')
426
427 return $icon
428 },
429
430 change_with_status_tree: function (change, changeQueue) {
431 let $changeRow = $('<tr />')
432
433 for (let i = 0; i < changeQueue._tree_columns; i++) {
434 let $treeCell = $('<td />')
435 .css('height', '100%')
436 .css('padding', '0 0 10px 0')
437 .css('margin', '0')
438 .css('width', '16px')
439 .css('min-width', '16px')
440 .css('overflow', 'hidden')
441 .css('vertical-align', 'top')
442
443 if (i < change._tree.length && change._tree[i] !== null) {
444 $treeCell.css('background-image',
445 'url(' + LineImage + ')')
446 .css('background-repeat', 'repeat-y')
447 }
448
449 if (i === change._tree_index) {
450 $treeCell.append(
451 this.change_status_icon(change))
452 }
453 if (change._tree_branches.indexOf(i) !== -1) {
454 let $image = $('<img />')
455 .css('vertical-align', 'baseline')
456 if (change._tree_branches.indexOf(i) ===
457 change._tree_branches.length - 1) {
458 // Angle line
459 $image.attr('src', LineAngleImage)
460 } else {
461 // T line
462 $image.attr('src', LineTImage)
463 }
464 $treeCell.append($image)
465 }
466 $changeRow.append($treeCell)
467 }
468
469 let changeWidth = 360 - 16 * changeQueue._tree_columns
470 let $changeColumn = $('<td />')
471 .css('width', changeWidth + 'px')
472 .addClass('zuul-change-cell')
473 .append(this.changePanel(change))
474
475 $changeRow.append($changeColumn)
476
477 let $changeTable = $('<table />')
478 .addClass('zuul-change-box')
479 .css('-moz-box-sizing', 'content-box')
480 .css('box-sizing', 'content-box')
481 .append($changeRow)
482
483 return $changeTable
484 },
485
486 pipeline_sparkline: function (pipelineName) {
487 if (options.graphite_url !== '') {
488 let $sparkline = $('<img />')
489 .addClass('pull-right')
490 .attr('src', getSparklineURL(pipelineName))
491 return $sparkline
492 }
493 return false
494 },
495
496 pipeline_header: function (pipeline, count) {
497 // Format the pipeline name, sparkline and description
498 let $headerDiv = $('<div />')
499 .addClass('zuul-pipeline-header')
500
501 let $heading = $('<h3 />')
502 .css('vertical-align', 'middle')
503 .text(pipeline.name)
504 .append(
505 $('<span />')
506 .addClass('badge pull-right')
507 .css('vertical-align', 'middle')
508 .css('margin-top', '0.5em')
509 .text(count)
510 )
511 .append(this.pipeline_sparkline(pipeline.name))
512
513 $headerDiv.append($heading)
514
515 if (typeof pipeline.description === 'string') {
516 let descr = $('<small />')
517 $.each(pipeline.description.split(/\r?\n\r?\n/),
518 function (index, descrPart) {
519 descr.append($('<p />').text(descrPart))
520 })
521 $headerDiv.append($('<p />').append(descr))
522 }
523 return $headerDiv
524 },
525
526 pipeline: function (pipeline, count) {
527 let format = this
528 let $html = $('<div />')
529 .addClass('zuul-pipeline col-md-4')
530 .append(this.pipeline_header(pipeline, count))
531
532 $.each(pipeline.change_queues, function (queueIndex, changeQueue) {
533 $.each(changeQueue.heads, function (headIndex, changes) {
534 if (pipeline.change_queues.length > 1 && headIndex === 0) {
535 let name = changeQueue.name
536 let shortName = name
537 if (shortName.length > 32) {
538 shortName = shortName.substr(0, 32) + '...'
539 }
540 $html.append($('<p />')
541 .text('Queue: ')
542 .append(
543 $('<abbr />')
544 .attr('title', name)
545 .text(shortName)
546 )
547 )
548 }
549
550 $.each(changes, function (changeIndex, change) {
551 let $changeBox =
552 format.change_with_status_tree(
553 change, changeQueue)
554 $html.append($changeBox)
555 format.display_patchset($changeBox)
556 })
557 })
558 })
559 return $html
560 },
561
562 toggle_patchset: function (e) {
563 // Toggle showing/hiding the patchset when the header is clicked.
564 if (e.target.nodeName.toLowerCase() === 'a') {
565 // Ignore clicks from gerrit patch set link
566 return
567 }
568
569 // Grab the patchset panel
570 let $panel = $(e.target).parents('.zuul-change')
571 let $body = $panel.children('.zuul-patchset-body')
572 $body.toggle(200)
573 let collapsedIndex = collapsedExceptions.indexOf(
574 $panel.attr('id'))
575 if (collapsedIndex === -1) {
576 // Currently not an exception, add it to list
577 collapsedExceptions.push($panel.attr('id'))
578 } else {
579 // Currently an except, remove from exceptions
580 collapsedExceptions.splice(collapsedIndex, 1)
581 }
582 },
583
584 display_patchset: function ($changeBox, animate) {
585 // Determine if to show or hide the patchset and/or the results
586 // when loaded
587
588 // See if we should hide the body/results
589 let $panel = $changeBox.find('.zuul-change')
590 let panelChange = $panel.attr('id')
591 let $body = $panel.children('.zuul-patchset-body')
592 let expandByDefault = $('#expand_by_default')
593 .prop('checked')
594
595 let collapsedIndex = collapsedExceptions
596 .indexOf(panelChange)
597
598 if ((expandByDefault && collapsedIndex === -1) ||
599 (!expandByDefault && collapsedIndex !== -1)) {
600 // Expand by default, or is an exception
601 $body.show(animate)
602 } else {
603 $body.hide(animate)
604 }
605
606 // Check if we should hide the whole panel
607 let panelProject = $panel.find('.change_project').text()
608 .toLowerCase()
609
610 let panelPipeline = $changeBox
611 .parents('.zuul-pipeline')
612 .find('.zuul-pipeline-header > h3')
613 .html()
614 .toLowerCase()
615
616 if (currentFilter !== '') {
617 let showPanel = false
618 let filter = currentFilter.trim().split(/[\s,]+/)
619 $.each(filter, function (index, filterVal) {
620 if (filterVal !== '') {
621 filterVal = filterVal.toLowerCase()
622 if (panelProject.indexOf(filterVal) !== -1 ||
623 panelPipeline.indexOf(filterVal) !== -1 ||
624 panelChange.indexOf(filterVal) !== -1) {
625 showPanel = true
626 }
627 }
628 })
629 if (showPanel === true) {
630 $changeBox.show(animate)
631 } else {
632 $changeBox.hide(animate)
633 }
634 } else {
635 $changeBox.show(animate)
636 }
637 }
638 }
639
640 let app = {
641 schedule: function (app) {
642 app = app || this
643 if (!options.enabled) {
644 setTimeout(function () { app.schedule(app) }, 5000)
645 return
646 }
647 app.update().always(function () {
648 setTimeout(function () { app.schedule(app) }, 5000)
649 })
650
651 // Only update graphs every minute
652 if (zuulGraphUpdateCount > 11) {
653 zuulGraphUpdateCount = 0
654 $.zuul.update_sparklines()
655 }
656 },
657 injest: function (data, $msg) {
658 if ('message' in data) {
659 $msg.removeClass('alert-danger')
660 .addClass('alert-info')
661 .text(data.message)
662 .show()
663 } else {
664 $msg.empty()
665 .hide()
666 }
667
668 if ('zuul_version' in data) {
669 $('#zuul-version-span').text(data.zuul_version)
670 }
671 if ('last_reconfigured' in data) {
672 let lastReconfigured =
673 new Date(data.last_reconfigured)
674 $('#last-reconfigured-span').text(
675 lastReconfigured.toString())
676 }
677
678 let $pipelines = $(options.pipelines_id)
679 $pipelines.html('')
680 $.each(data.pipelines, function (i, pipeline) {
681 let count = app.create_tree(pipeline)
682 $pipelines.append(
683 format.pipeline(pipeline, count))
684 })
685
686 $(options.queue_events_num).text(
687 data.trigger_event_queue
688 ? data.trigger_event_queue.length : '0'
689 )
690 $(options.queue_results_num).text(
691 data.result_event_queue
692 ? data.result_event_queue.length : '0'
693 )
694 },
695 /** @return {jQuery.Promise} */
696 update: function () {
697 // Cancel the previous update if it hasn't completed yet.
698 if (xhr) {
699 xhr.abort()
700 }
701
702 this.emit('update-start')
703 let app = this
704
705 let $msg = $(options.msg_id)
706 if (options.source_data !== null) {
707 app.injest(options.source_data, $msg)
708 return
709 }
710 xhr = $.getJSON(options.source)
711 .done(function (data) {
712 app.injest(data, $msg)
713 })
714 .fail(function (jqXHR, statusText, errMsg) {
715 if (statusText === 'abort') {
716 return
717 }
718 $msg.text(options.source + ': ' + errMsg)
719 .addClass('alert-danger')
720 .removeClass('zuul-msg-wrap-off')
721 .show()
722 })
723 .always(function () {
724 xhr = undefined
725 app.emit('update-end')
726 })
727
728 return xhr
729 },
730
731 update_sparklines: function () {
732 $.each(zuulSparklineURLs, function (name, url) {
733 let newimg = new Image()
734 let parts = url.split('#')
735 newimg.src = parts[0] + '#' + new Date().getTime()
736 $(newimg).load(function () {
737 zuulSparklineURLs[name] = newimg.src
738 })
739 })
740 },
741
742 emit: function () {
743 $jq.trigger.apply($jq, arguments)
744 return this
745 },
746 on: function () {
747 $jq.on.apply($jq, arguments)
748 return this
749 },
750 one: function () {
751 $jq.one.apply($jq, arguments)
752 return this
753 },
754
755 controlForm: function () {
756 // Build the filter form filling anything from cookies
757
758 let $controlForm = $('<form />')
759 .attr('role', 'form')
760 .addClass('form-inline')
761 .submit(this.handleFilterChange)
762
763 $controlForm
764 .append(this.filterFormGroup())
765 .append(this.expandFormGroup())
766
767 return $controlForm
768 },
769
770 filterFormGroup: function () {
771 // Update the filter form with a clear button if required
772
773 let $label = $('<label />')
774 .addClass('control-label')
775 .attr('for', 'filter_string')
776 .text('Filters')
777 .css('padding-right', '0.5em')
778
779 let $input = $('<input />')
780 .attr('type', 'text')
781 .attr('id', 'filter_string')
782 .addClass('form-control')
783 .attr('title',
784 'project(s), pipeline(s) or review(s) comma ' +
785 'separated')
786 .attr('value', currentFilter)
787
788 $input.change(this.handleFilterChange)
789
790 let $clearIcon = $('<span />')
791 .addClass('form-control-feedback')
792 .addClass('glyphicon glyphicon-remove-circle')
793 .attr('id', 'filter_form_clear_box')
794 .attr('title', 'clear filter')
795 .css('cursor', 'pointer')
796
797 $clearIcon.click(function () {
798 $('#filter_string').val('').change()
799 })
800
801 if (currentFilter === '') {
802 $clearIcon.hide()
803 }
804
805 let $formGroup = $('<div />')
806 .addClass('form-group has-feedback')
807 .append($label, $input, $clearIcon)
808 return $formGroup
809 },
810
811 expandFormGroup: function () {
812 let expandByDefault = (
813 readCookie('zuul_expand_by_default', false) === 'true')
814
815 let $checkbox = $('<input />')
816 .attr('type', 'checkbox')
817 .attr('id', 'expand_by_default')
818 .prop('checked', expandByDefault)
819 .change(this.handleExpandByDefault)
820
821 let $label = $('<label />')
822 .css('padding-left', '1em')
823 .html('Expand by default: ')
824 .append($checkbox)
825
826 let $formGroup = $('<div />')
827 .addClass('checkbox')
828 .append($label)
829 return $formGroup
830 },
831
832 handleFilterChange: function () {
833 // Update the filter and save it to a cookie
834 currentFilter = $('#filter_string').val()
835 setCookie('zuul_filter_string', currentFilter)
836 if (currentFilter === '') {
837 $('#filter_form_clear_box').hide()
838 } else {
839 $('#filter_form_clear_box').show()
840 }
841
842 $('.zuul-change-box').each(function (index, obj) {
843 let $changeBox = $(obj)
844 format.display_patchset($changeBox, 200)
845 })
846 return false
847 },
848
849 handleExpandByDefault: function (e) {
850 // Handle toggling expand by default
851 setCookie('zuul_expand_by_default', e.target.checked)
852 collapsedExceptions = []
853 $('.zuul-change-box').each(function (index, obj) {
854 let $changeBox = $(obj)
855 format.display_patchset($changeBox, 200)
856 })
857 },
858
859 create_tree: function (pipeline) {
860 let count = 0
861 let pipelineMaxTreeColumns = 1
862 $.each(pipeline.change_queues,
863 function (changeQueueIndex, changeQueue) {
864 let tree = []
865 let maxTreeColumns = 1
866 let changes = []
867 let lastTreeLength = 0
868 $.each(changeQueue.heads, function (headIndex, head) {
869 $.each(head, function (changeIndex, change) {
870 changes[change.id] = change
871 change._tree_position = changeIndex
872 })
873 })
874 $.each(changeQueue.heads, function (headIndex, head) {
875 $.each(head, function (changeIndex, change) {
876 if (change.live === true) {
877 count += 1
878 }
879 let idx = tree.indexOf(change.id)
880 if (idx > -1) {
881 change._tree_index = idx
882 // remove...
883 tree[idx] = null
884 while (tree[tree.length - 1] === null) {
885 tree.pop()
886 }
887 } else {
888 change._tree_index = 0
889 }
890 change._tree_branches = []
891 change._tree = []
892 if (typeof (change.items_behind) === 'undefined') {
893 change.items_behind = []
894 }
895 change.items_behind.sort(function (a, b) {
896 return (changes[b]._tree_position - changes[a]._tree_position)
897 })
898 $.each(change.items_behind, function (i, id) {
899 tree.push(id)
900 if (tree.length > lastTreeLength && lastTreeLength > 0) {
901 change._tree_branches.push(tree.length - 1)
902 }
903 })
904 if (tree.length > maxTreeColumns) {
905 maxTreeColumns = tree.length
906 }
907 if (tree.length > pipelineMaxTreeColumns) {
908 pipelineMaxTreeColumns = tree.length
909 }
910 change._tree = tree.slice(0) // make a copy
911 lastTreeLength = tree.length
912 })
913 })
914 changeQueue._tree_columns = maxTreeColumns
915 })
916 pipeline._tree_columns = pipelineMaxTreeColumns
917 return count
918 }
919 }
920
921 $jq = $(app)
922 return {
923 options: options,
924 format: format,
925 app: app,
926 jq: $jq
927 }
928 }
929}(jQuery))