go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/frontend/static/common/js/build.js (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  
    16  // Code used for managing how nested steps are rendered in build view. In
    17  // particular, it implement collapsing/nesting functionality and various modes
    18  // for the Show option.
    19  
    20  $(document).ready(function() {
    21    'use strict';
    22  
    23    $('li.substeps').each(function() {
    24      const substep = $(this);
    25      $(this).find('>div.result').click(function() {
    26        substep.toggleClass('collapsed');
    27      });
    28    });
    29  
    30    function setCookie(name, value) {
    31      const farFuture = new Date(2100, 0).toUTCString();
    32      document.cookie = `${name}=${value}; expires=${farFuture}; path=/`;
    33    }
    34  
    35    function displayPrefChanged(selectedOption) {
    36      if (!['expanded', 'default', 'non-green'].includes(selectedOption)) {
    37        selectedOption = 'default';
    38      }
    39  
    40      $('li.substeps').removeClass('collapsed');
    41      $('#steps').removeClass('non-green');
    42  
    43      switch (selectedOption) {
    44        case 'expanded':
    45          break;
    46        case 'default':
    47          $('li.substeps.green').addClass('collapsed');
    48          break;
    49        case 'non-green':
    50          $('#steps').addClass('non-green');
    51          break;
    52      }
    53  
    54      setCookie('stepDisplayPref', selectedOption);
    55    }
    56  
    57    const newBuildPageLink = $('#new-build-page-link');
    58    newBuildPageLink.on('click', (e) => {
    59      if (e.metaKey || e.shiftKey || e.ctrlKey || e.altKey) {
    60        return true;
    61      }
    62  
    63      setCookie('showNewBuildPage', true);
    64  
    65      if ('serviceWorker' in navigator) {
    66        navigator.serviceWorker.register('/root_sw.js')
    67          .then((registration) => {
    68            console.log('Root SW registered: ', registration);
    69            window.open(newBuildPageLink.attr('href'), newBuildPageLink.attr('target') || '_self');
    70          });
    71        return false;
    72      }
    73      return true;
    74    });
    75  
    76    displayPrefChanged($('input[name="hider"]:checked').val());
    77    $('input[name="hider"]').change(function() {displayPrefChanged($(this).val())});
    78  
    79    $('#showDebugLogs').change(function(e) {
    80      setCookie('showDebugLogsPref', this.checked)
    81      $('#steps').toggleClass('hide-debug-logs', !this.checked);
    82    });
    83  
    84    function createTimeline() {
    85      function groupTemplater(group, element, data) {
    86        return `
    87          <div class="group-title ${group.data.statusClassName}">
    88            <span class="title">${group.data.label}</span>
    89            <span class="duration">( ${group.data.duration} )</span>
    90          </div>`;
    91      }
    92  
    93      const options = {
    94        clickToUse: false,
    95        groupTemplate: groupTemplater,
    96        multiselect: false,
    97        onInitialDrawComplete: () => {
    98          $('#timeline-rendering').remove();
    99        },
   100        orientation: {
   101          axis: 'both',
   102          item: 'top',
   103        },
   104        template: item => item.data.label,
   105        zoomable: false,
   106      };
   107  
   108      const timeline = new vis.Timeline(
   109          document.getElementById('timeline'),
   110          new vis.DataSet(timelineData.items),
   111          new vis.DataSet(timelineData.groups),
   112          options);
   113      timeline.on('select', function(props) {
   114        const item = timeline.itemsData.get(props.items[0]);
   115        if (!item) {
   116          return;
   117        }
   118        if (item.data && item.data.logUrl) {
   119          window.open(item.data.logUrl, '_blank');
   120        }
   121      });
   122  
   123      return timeline;
   124    }
   125  
   126    let timeline = null;
   127    let relatedBuilds = null;
   128  
   129    // By hiding the tab div until the tabs are constructed a flicker
   130    // of the tab contents not within the tabs is avoided.
   131    $('#tabs').tabs({
   132      activate: function(event, ui) {
   133        switch (ui.newPanel.attr('id')) {
   134          // By lazily creating the timeline only when its tab is first activated an
   135          // ugly multi-step rendering (and console warning of infinite redraw loop)
   136          // of the timeline is avoided. This could also be avoided by making the
   137          // timeline tab the initially visible tab and creating the timeline just
   138          // after the tabs are initialized (that is, wouldn't have to use this
   139          // activate event hack).
   140          case 'timeline-tab':
   141            if (timeline === null) {
   142              timeline = createTimeline();
   143            }
   144            break;
   145  
   146          case 'blamelist-tab':
   147            if (!blamelistLoaded) {
   148              const params = (new URL(window.location)).searchParams;
   149              params.set('blamelist', '1');
   150              document.location = `?${params.toString()}#blamelist-tab`;
   151            }
   152            break;
   153  
   154          // Load related builds asynchronously so it doesn't block rendering the rest of the page.
   155          case 'related-tab':
   156            const tableContainerRef = $('#related-builds-table-container')
   157            if (!relatedBuilds && tableContainerRef) {
   158              relatedBuilds = fetch(`/internal_widgets/related_builds/${buildbucketId}`)
   159                .then((res) => res.text())
   160                .then((tablehtml) => tableContainerRef.html(tablehtml))
   161                .catch((_) => tableContainerRef.html('<p style="color: red;">Something went wrong</p>'))
   162            }
   163            break;
   164        }
   165      },
   166    }).show();
   167  
   168    const closeModal = () => {
   169      $('#modal-overlay')
   170        .removeClass('show')
   171        .removeClass('show-retry-build-modal')
   172        .removeClass('show-cancel-build-modal');
   173    };
   174  
   175    $('#retry-build-button').click(e => {
   176      closeModal();
   177      $('#modal-overlay')
   178        .addClass('show')
   179        .addClass('show-retry-build-modal');
   180    });
   181    $('#dismiss-retry-build-button').click(closeModal);
   182  
   183    $('#cancel-build-button').click(e => {
   184      closeModal();
   185      $('#modal-overlay')
   186        .addClass('show')
   187        .addClass('show-cancel-build-modal');
   188    });
   189    $('#dismiss-cancel-build-button').click(closeModal);
   190  
   191    $('#modal-overlay').click(e => {
   192      if ($(e.target).is('#modal-overlay')) {
   193        closeModal();
   194      }
   195    });
   196  
   197    if (!document.cookie.includes('askForFeedback=false')) {
   198      $('#feedback-bar').toggleClass('show', true);
   199    }
   200  
   201    $('#dismiss-feedback-bar').click(e => {
   202      $('#feedback-bar').toggleClass('show', false);
   203  
   204      const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString();
   205      document.cookie = `askForFeedback=false; expires=${expires}; path=/`;
   206      e.preventDefault();
   207    });
   208  
   209    $('#feedback-link').click(e => {
   210      const feedbackComment = encodeURIComponent(
   211  `From Link: ${document.location.href}
   212  Please enter a description of the problem, with repro steps if applicable.
   213  `);
   214      const url = `https://issuetracker.google.com/issues/new?component=1456503&type=BUG&priority=P2&severity=S2&inProd=true&description=${feedbackComment}`;
   215      window.open(url);
   216      e.preventDefault();
   217    });
   218  
   219    // Keep the new build page version up-to-date even when the user is using the
   220    // legacy build page.
   221    // Otherwise the new build page version will not take effect until the user
   222    // loads the new build page for the 2nd time due to service worker activation
   223    // rules.
   224    navigator.serviceWorker?.register('/ui/ui_sw.js')
   225      .then((registration) => registration.update())
   226      .then((registration) => {
   227        console.log('New build page SW registered: ', registration);
   228      });
   229  });