<commit_before>/* eslint-disable no-new, class-methods-use-this */
/* global notes */

import Cookies from 'js-cookie';
import Flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import {
  parseUrlPathname,
  handleLocationHash,
  isMetaClick,
} from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';

/* eslint-disable max-len */
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page.
//
// ### Example Markup
//
//

//

//
//         Discussion
//
//

//

//
//         Commits
//
//

//

//
//         Diffs
//
//

//

//
//

//

//       Notes Content
//

//

//       Commits Content
//

//

//       Diffs Content
//

//

//
//

//

//       Loading Animation
//

//

//
/* eslint-enable max-len */

(() => {
  // Store the `location` object, allowing for easier stubbing in tests
  let location = window.location;

  class MergeRequestTabs {

    constructor({ action, setUrl, stubLocation } = {}) {
      const mergeRequestTabs = document.querySelector('.js-tabs-affix');
      const paddingTop = 16;

      this.diffsLoaded = false;
      this.pipelinesLoaded = false;
      this.commitsLoaded = false;
      this.fixedLayoutPref = null;

      this.setUrl = setUrl !== undefined ? setUrl : true;
      this.setCurrentAction = this.setCurrentAction.bind(this);
      this.tabShown = this.tabShown.bind(this);
      this.showTab = this.showTab.bind(this);
      this.stickyTop = document.querySelector('.navbar-gitlab').offsetHeight - paddingTop;

      if (mergeRequestTabs) {
        this.stickyTop += mergeRequestTabs.offsetHeight;
      }

      if (stubLocation) {
        location = stubLocation;
      }

      this.bindEvents();
      this.activateTab(action);
      this.initAffix();
    }

    bindEvents() {
      $(document)
        .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
        .on('click', '.js-show-tab', this.showTab);

      $('.merge-request-tabs a[data-toggle="tab"]')
        .on('click', this.clickTab);
    }

    // Used in tests
    unbindEvents() {
      $(document)
        .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
        .off('click', '.js-show-tab', this.showTab);

      $('.merge-request-tabs a[data-toggle="tab"]')
        .off('click', this.clickTab);
    }

    destroyPipelinesView() {
      if (this.commitPipelinesTable) {
        this.commitPipelinesTable.$destroy();
        this.commitPipelinesTable = null;

        document.querySelector('#commit-pipeline-table-view').innerHTML = '';
      }
    }

    showTab(e) {
      e.preventDefault();
      this.activateTab($(e.target).data('action'));
    }

    clickTab(e) {
      if (e.currentTarget && isMetaClick(e)) {
        const targetLink = e.currentTarget.getAttribute('href');
        e.stopImmediatePropagation();
        e.preventDefault();
        window.open(targetLink, '_blank');
      }
    }

    tabShown(e) {
      const $target = $(e.target);
      const action = $target.data('action');

      if (action === 'commits') {
        this.loadCommits($target.attr('href'));
        this.expandView();
        this.resetViewContainer();
        this.destroyPipelinesView();
      } else if (this.isDiffAction(action)) {
        this.loadDiff($target.attr('href'));
        if (bp.getBreakpointSize() !== 'lg') {
          this.shrinkView();
        }
        if (this.diffViewType() === 'parallel') {
          this.expandViewContainer();
        }
        this.destroyPipelinesView();
      } else if (action === 'pipelines') {
        this.resetViewContainer();
        this.mountPipelinesView();
      } else {
        if (bp.getBreakpointSize() !== 'xs') {
          this.expandView();
        }
        this.resetViewContainer();
        this.destroyPipelinesView();

        initDiscussionTab();
      }
      if (this.setUrl) {
        this.setCurrentAction(action);
      }
    }

    scrollToElement(container) {
      if (location.hash) {
        const offset = 0 - (
          $('.navbar-gitlab').outerHeight() +
          $('.js-tabs-affix').outerHeight()
        );
        const $el = $(`${container} ${location.hash}:not(.match)`);
        if ($el.length) {
          $.scrollTo($el[0], { offset });
        }
      }
    }

    // Activate a tab based on the current action
    activateTab(action) {
      // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
      $(`.merge-request-tabs a[data-action='${action}']`).tab('show');
    }

    // Replaces the current Merge Request-specific action in the URL with a new one
    //
    // If the action is "notes", the URL is reset to the standard
    // `MergeRequests#show` route.
    //
    // Examples:
    //
    //   location.pathname # => "/namespace/project/merge_requests/1"
    //   setCurrentAction('diffs')
    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
    //
    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
    //   setCurrentAction('show')
    //   location.pathname # => "/namespace/project/merge_requests/1"
    //
    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
    //   setCurrentAction('commits')
    //   location.pathname # => "/namespace/project/merge_requests/1/commits"
    //
    // Returns the new URL String
    setCurrentAction(action) {
      this.currentAction = action;

      // Remove a trailing '/commits' '/diffs' '/pipelines'
      let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');

      // Append the new action if we're on a tab other than 'notes'
      if (this.currentAction !== 'show' && this.currentAction !== 'new') {
        newState += `/${this.currentAction}`;
      }

      // Ensure parameters and hash come along for the ride
      newState += location.search + location.hash;

      // TODO: Consider refactoring in light of turbolinks removal.

      // Replace the current history state with the new one without breaking
      // Turbolinks' history.
      //
      // See https://github.com/rails/turbolinks/issues/363
      window.history.replaceState({
        url: newState,
      }, document.title, newState);

      return newState;
    }

    loadCommits(source) {
      if (this.commitsLoaded) {
        return;
      }
      this.ajaxGet({
        url: `${source}.json`,
        success: (data) => {
          document.querySelector('div#commits').innerHTML = data.html;
          gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
          this.commitsLoaded = true;
          this.scrollToElement('#commits');
        },
      });
    }

    mountPipelinesView() {
      const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
      const CommitPipelinesTable = gl.CommitPipelinesTable;
      this.commitPipelinesTable = new CommitPipelinesTable({
        propsData: {
          endpoint: pipelineTableViewEl.dataset.endpoint,
          helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
          emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
          errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
          autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
        },
      }).$mount();

      // $mount(el) replaces the el with the new rendered component. We need it in order to mount
      // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
      pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
    }

    loadDiff(source) {
      if (this.diffsLoaded) {
        document.dispatchEvent(new CustomEvent('scroll'));
        return;
      }

      // We extract pathname for the current Changes tab anchor href
      // some pages like MergeRequestsController#new has query parameters on that anchor
      const urlPathname = parseUrlPathname(source);

      this.ajaxGet({
        url: `${urlPathname}.json${location.search}`,
        success: (data) => {
          const $container = $('#diffs');
          $container.html(data.html);

          initChangesDropdown(this.stickyTop);

          if (typeof gl.diffNotesCompileComponents !== 'undefined') {
            gl.diffNotesCompileComponents();
          }

          gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
          $('#diffs .js-syntax-highlight').syntaxHighlight();

          if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
            this.expandViewContainer();
          }
          this.diffsLoaded = true;

          new Diff();
          this.scrollToElement('#diffs');

          $('.diff-file').each((i, el) => {
            new BlobForkSuggestion({
              openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
              forkButtons: $(el).find('.js-fork-suggestion-button'),
              cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
              suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
              actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
            })
              .init();
          });

          // Scroll any linked note into view
          // Similar to `toggler_behavior` in the discussion tab
          const hash = window.gl.utils.getLocationHash();
          const anchor = hash && $container.find(`.note[id="${hash}"]`);
          if (anchor && anchor.length > 0) {
            const notesContent = anchor.closest('.notes_content');
            const lineType = notesContent.hasClass('new') ? 'new' : 'old';
            notes.toggleDiffNote({
              target: anchor,
              lineType,
              forceShow: true,
            });
            anchor[0].scrollIntoView();
            handleLocationHash();
            // We have multiple elements on the page with `#note_xxx`
            // (discussion and diff tabs) and `:target` only applies to the first
            anchor.addClass('target');
          }
        },
      });
    }

    // Show or hide the loading spinner
    //
    // status - Boolean, true to show, false to hide
    toggleLoading(status) {
      $('.mr-loading-status .loading').toggle(status);
    }

    ajaxGet(options) {
      const defaults = {
        beforeSend: () => this.toggleLoading(true),
        error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
        complete: () => this.toggleLoading(false),
        dataType: 'json',
        type: 'GET',
      };
      $.ajax($.extend({}, defaults, options));
    }

    diffViewType() {
      return $('.inline-parallel-buttons a.active').data('view-type');
    }

    isDiffAction(action) {
      return action === 'diffs' || action === 'new/diffs';
    }

    expandViewContainer() {
      const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
      if (this.fixedLayoutPref === null) {
        this.fixedLayoutPref = $wrapper.hasClass('container-limited');
      }
      $wrapper.removeClass('container-limited');
    }

    resetViewContainer() {
      if (this.fixedLayoutPref !== null) {
        $('.content-wrapper .container-fluid')
          .toggleClass('container-limited', this.fixedLayoutPref);
      }
    }

    shrinkView() {
      const $gutterIcon = $('.js-sidebar-toggle i:visible');

      // Wait until listeners are set
      setTimeout(() => {
        // Only when sidebar is expanded
        if ($gutterIcon.is('.fa-angle-double-right')) {
          $gutterIcon.closest('a').trigger('click', [true]);
        }
      }, 0);
    }

    // Expand the issuable sidebar unless the user explicitly collapsed it
    expandView() {
      if (Cookies.get('collapsed_gutter') === 'true') {
        return;
      }
      const $gutterIcon = $('.js-sidebar-toggle i:visible');

      // Wait until listeners are set
      setTimeout(() => {
        // Only when sidebar is collapsed
        if ($gutterIcon.is('.fa-angle-double-left')) {
          $gutterIcon.closest('a').trigger('click', [true]);
        }
      }, 0);
    }

    initAffix() {
      const $tabs = $('.js-tabs-affix');
      const $fixedNav = $('.navbar-gitlab');

      // Screen space on small screens is usually very sparse
      // So we dont affix the tabs on these
      if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;

      /**
        If the browser does not support position sticky, it returns the position as static.
        If the browser does support sticky, then we allow the browser to handle it, if not
        then we default back to Bootstraps affix
      **/
      if ($tabs.css('position') !== 'static') return;

      const $diffTabs = $('#diff-notes-app');

      $tabs.off('affix.bs.affix affix-top.bs.affix')
        .affix({
          offset: {
            top: () => (
              $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
            ),
          },
        })
        .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
        .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));

      // Fix bug when reloading the page already scrolling
      if ($tabs.hasClass('affix')) {
        $tabs.trigger('affix.bs.affix');
      }
    }
  }

  window.gl = window.gl || {};
  window.gl.MergeRequestTabs = MergeRequestTabs;
})();
<commit_msg>fixed karma tests<commit_after>/* eslint-disable no-new, class-methods-use-this */
/* global notes */

import Cookies from 'js-cookie';
import Flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import {
  parseUrlPathname,
  handleLocationHash,
  isMetaClick,
} from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';

/* eslint-disable max-len */
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page.
//
// ### Example Markup
//
//

//

//
//         Discussion
//
//

//

//
//         Commits
//
//

//

//
//         Diffs
//
//

//

//
//

//

//       Notes Content
//

//

//       Commits Content
//

//

//       Diffs Content
//

//

//
//

//

//       Loading Animation
//

//

//
/* eslint-enable max-len */

(() => {
  // Store the `location` object, allowing for easier stubbing in tests
  let location = window.location;

  class MergeRequestTabs {

    constructor({ action, setUrl, stubLocation } = {}) {
      const mergeRequestTabs = document.querySelector('.js-tabs-affix');
      const navbar = document.querySelector('.navbar-gitlab');
      const paddingTop = 16;

      this.diffsLoaded = false;
      this.pipelinesLoaded = false;
      this.commitsLoaded = false;
      this.fixedLayoutPref = null;

      this.setUrl = setUrl !== undefined ? setUrl : true;
      this.setCurrentAction = this.setCurrentAction.bind(this);
      this.tabShown = this.tabShown.bind(this);
      this.showTab = this.showTab.bind(this);
      this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;

      if (mergeRequestTabs) {
        this.stickyTop += mergeRequestTabs.offsetHeight;
      }

      if (stubLocation) {
        location = stubLocation;
      }

      this.bindEvents();
      this.activateTab(action);
      this.initAffix();
    }

    bindEvents() {
      $(document)
        .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
        .on('click', '.js-show-tab', this.showTab);

      $('.merge-request-tabs a[data-toggle="tab"]')
        .on('click', this.clickTab);
    }

    // Used in tests
    unbindEvents() {
      $(document)
        .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
        .off('click', '.js-show-tab', this.showTab);

      $('.merge-request-tabs a[data-toggle="tab"]')
        .off('click', this.clickTab);
    }

    destroyPipelinesView() {
      if (this.commitPipelinesTable) {
        this.commitPipelinesTable.$destroy();
        this.commitPipelinesTable = null;

        document.querySelector('#commit-pipeline-table-view').innerHTML = '';
      }
    }

    showTab(e) {
      e.preventDefault();
      this.activateTab($(e.target).data('action'));
    }

    clickTab(e) {
      if (e.currentTarget && isMetaClick(e)) {
        const targetLink = e.currentTarget.getAttribute('href');
        e.stopImmediatePropagation();
        e.preventDefault();
        window.open(targetLink, '_blank');
      }
    }

    tabShown(e) {
      const $target = $(e.target);
      const action = $target.data('action');

      if (action === 'commits') {
        this.loadCommits($target.attr('href'));
        this.expandView();
        this.resetViewContainer();
        this.destroyPipelinesView();
      } else if (this.isDiffAction(action)) {
        this.loadDiff($target.attr('href'));
        if (bp.getBreakpointSize() !== 'lg') {
          this.shrinkView();
        }
        if (this.diffViewType() === 'parallel') {
          this.expandViewContainer();
        }
        this.destroyPipelinesView();
      } else if (action === 'pipelines') {
        this.resetViewContainer();
        this.mountPipelinesView();
      } else {
        if (bp.getBreakpointSize() !== 'xs') {
          this.expandView();
        }
        this.resetViewContainer();
        this.destroyPipelinesView();

        initDiscussionTab();
      }
      if (this.setUrl) {
        this.setCurrentAction(action);
      }
    }

    scrollToElement(container) {
      if (location.hash) {
        const offset = 0 - (
          $('.navbar-gitlab').outerHeight() +
          $('.js-tabs-affix').outerHeight()
        );
        const $el = $(`${container} ${location.hash}:not(.match)`);
        if ($el.length) {
          $.scrollTo($el[0], { offset });
        }
      }
    }

    // Activate a tab based on the current action
    activateTab(action) {
      // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
      $(`.merge-request-tabs a[data-action='${action}']`).tab('show');
    }

    // Replaces the current Merge Request-specific action in the URL with a new one
    //
    // If the action is "notes", the URL is reset to the standard
    // `MergeRequests#show` route.
    //
    // Examples:
    //
    //   location.pathname # => "/namespace/project/merge_requests/1"
    //   setCurrentAction('diffs')
    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
    //
    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
    //   setCurrentAction('show')
    //   location.pathname # => "/namespace/project/merge_requests/1"
    //
    //   location.pathname # => "/namespace/project/merge_requests/1/diffs"
    //   setCurrentAction('commits')
    //   location.pathname # => "/namespace/project/merge_requests/1/commits"
    //
    // Returns the new URL String
    setCurrentAction(action) {
      this.currentAction = action;

      // Remove a trailing '/commits' '/diffs' '/pipelines'
      let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');

      // Append the new action if we're on a tab other than 'notes'
      if (this.currentAction !== 'show' && this.currentAction !== 'new') {
        newState += `/${this.currentAction}`;
      }

      // Ensure parameters and hash come along for the ride
      newState += location.search + location.hash;

      // TODO: Consider refactoring in light of turbolinks removal.

      // Replace the current history state with the new one without breaking
      // Turbolinks' history.
      //
      // See https://github.com/rails/turbolinks/issues/363
      window.history.replaceState({
        url: newState,
      }, document.title, newState);

      return newState;
    }

    loadCommits(source) {
      if (this.commitsLoaded) {
        return;
      }
      this.ajaxGet({
        url: `${source}.json`,
        success: (data) => {
          document.querySelector('div#commits').innerHTML = data.html;
          gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
          this.commitsLoaded = true;
          this.scrollToElement('#commits');
        },
      });
    }

    mountPipelinesView() {
      const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
      const CommitPipelinesTable = gl.CommitPipelinesTable;
      this.commitPipelinesTable = new CommitPipelinesTable({
        propsData: {
          endpoint: pipelineTableViewEl.dataset.endpoint,
          helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
          emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
          errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
          autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
        },
      }).$mount();

      // $mount(el) replaces the el with the new rendered component. We need it in order to mount
      // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
      pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
    }

    loadDiff(source) {
      if (this.diffsLoaded) {
        document.dispatchEvent(new CustomEvent('scroll'));
        return;
      }

      // We extract pathname for the current Changes tab anchor href
      // some pages like MergeRequestsController#new has query parameters on that anchor
      const urlPathname = parseUrlPathname(source);

      this.ajaxGet({
        url: `${urlPathname}.json${location.search}`,
        success: (data) => {
          const $container = $('#diffs');
          $container.html(data.html);

          initChangesDropdown(this.stickyTop);

          if (typeof gl.diffNotesCompileComponents !== 'undefined') {
            gl.diffNotesCompileComponents();
          }

          gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
          $('#diffs .js-syntax-highlight').syntaxHighlight();

          if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
            this.expandViewContainer();
          }
          this.diffsLoaded = true;

          new Diff();
          this.scrollToElement('#diffs');

          $('.diff-file').each((i, el) => {
            new BlobForkSuggestion({
              openButtons: $(el).find('.js-edit-blob-link-fork-toggler'),
              forkButtons: $(el).find('.js-fork-suggestion-button'),
              cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
              suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
              actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
            })
              .init();
          });

          // Scroll any linked note into view
          // Similar to `toggler_behavior` in the discussion tab
          const hash = window.gl.utils.getLocationHash();
          const anchor = hash && $container.find(`.note[id="${hash}"]`);
          if (anchor && anchor.length > 0) {
            const notesContent = anchor.closest('.notes_content');
            const lineType = notesContent.hasClass('new') ? 'new' : 'old';
            notes.toggleDiffNote({
              target: anchor,
              lineType,
              forceShow: true,
            });
            anchor[0].scrollIntoView();
            handleLocationHash();
            // We have multiple elements on the page with `#note_xxx`
            // (discussion and diff tabs) and `:target` only applies to the first
            anchor.addClass('target');
          }
        },
      });
    }

    // Show or hide the loading spinner
    //
    // status - Boolean, true to show, false to hide
    toggleLoading(status) {
      $('.mr-loading-status .loading').toggle(status);
    }

    ajaxGet(options) {
      const defaults = {
        beforeSend: () => this.toggleLoading(true),
        error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
        complete: () => this.toggleLoading(false),
        dataType: 'json',
        type: 'GET',
      };
      $.ajax($.extend({}, defaults, options));
    }

    diffViewType() {
      return $('.inline-parallel-buttons a.active').data('view-type');
    }

    isDiffAction(action) {
      return action === 'diffs' || action === 'new/diffs';
    }

    expandViewContainer() {
      const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
      if (this.fixedLayoutPref === null) {
        this.fixedLayoutPref = $wrapper.hasClass('container-limited');
      }
      $wrapper.removeClass('container-limited');
    }

    resetViewContainer() {
      if (this.fixedLayoutPref !== null) {
        $('.content-wrapper .container-fluid')
          .toggleClass('container-limited', this.fixedLayoutPref);
      }
    }

    shrinkView() {
      const $gutterIcon = $('.js-sidebar-toggle i:visible');

      // Wait until listeners are set
      setTimeout(() => {
        // Only when sidebar is expanded
        if ($gutterIcon.is('.fa-angle-double-right')) {
          $gutterIcon.closest('a').trigger('click', [true]);
        }
      }, 0);
    }

    // Expand the issuable sidebar unless the user explicitly collapsed it
    expandView() {
      if (Cookies.get('collapsed_gutter') === 'true') {
        return;
      }
      const $gutterIcon = $('.js-sidebar-toggle i:visible');

      // Wait until listeners are set
      setTimeout(() => {
        // Only when sidebar is collapsed
        if ($gutterIcon.is('.fa-angle-double-left')) {
          $gutterIcon.closest('a').trigger('click', [true]);
        }
      }, 0);
    }

    initAffix() {
      const $tabs = $('.js-tabs-affix');
      const $fixedNav = $('.navbar-gitlab');

      // Screen space on small screens is usually very sparse
      // So we dont affix the tabs on these
      if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;

      /**
        If the browser does not support position sticky, it returns the position as static.
        If the browser does support sticky, then we allow the browser to handle it, if not
        then we default back to Bootstraps affix
      **/
      if ($tabs.css('position') !== 'static') return;

      const $diffTabs = $('#diff-notes-app');

      $tabs.off('affix.bs.affix affix-top.bs.affix')
        .affix({
          offset: {
            top: () => (
              $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
            ),
          },
        })
        .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
        .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));

      // Fix bug when reloading the page already scrolling
      if ($tabs.hasClass('affix')) {
        $tabs.trigger('affix.bs.affix');
      }
    }
  }

  window.gl = window.gl || {};
  window.gl.MergeRequestTabs = MergeRequestTabs;
})();
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2017 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'AltView',
  extends: 'foam.u2.View',

  documentation: 'Provides the ability to switch between multiple views for data set',

  requires: [
    'foam.u2.stack.Stack'
  ],

  properties: [
    {
      name: 'of',
      factory: function() { return this.data.of }
    },
    {
      name: 'views',
      value: []
    },
    {
      name: 'altStack'
    },
    {
      name: 'viewChoices',
      view: function(_, X) {
        return foam.u2.view.ChoiceView.create({
          choices: X.data.views
        });
      }
    },
  ],

  axioms: [
    foam.u2.CSS.create({
      code: function CSS() {/*
        ^ {
          width: 992px;
          margin: auto;
        }
        ^ .foam-u2-tag-Select{
          width: 100px;
          height: 40px;
        }
        ^ .property-viewChoices{
          margin: 0 0 25px 15px;
        }
      */}
    })
  ],

  methods: [
    function initE() {
      this.SUPER();
      this.altStack = null;
      this.altStack = this.Stack.create();
      this.viewChoices$.sub(this.changeView);

      this.addClass(this.myClass())
      this.startContext({data: this})
        this.start()
          .add(this.VIEW_CHOICES)
        .end()
      .endContext()
      .start('div').addClass('stack-wrapper')
        .tag({class: 'foam.u2.stack.StackView', data: this.altStack, showActions: false})
      .end()

      if(!this.views[0]){
        this.views.push([ { class: 'foam.u2.view.TableView' , data: this.data }, 'Table' ]);
      } else {
        this.altStack.push(this.views[0][0]);
      }
    }
  ],

  listeners: [
    function changeView(){
      this.altStack.push(this.viewChoices);
    }
  ]
});<commit_msg>alt view<commit_after>/**
 * @license
 * Copyright 2017 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'AltView',
  extends: 'foam.u2.View',

  documentation: 'Provides the ability to switch between multiple views for data set',

  requires: [
    'foam.u2.stack.Stack'
  ],

  properties: [
    {
      name: 'of',
      factory: function() { return this.data.of }
    },
    {
      name: 'views',
      value: []
    },
    {
      name: 'altStack'
    },
    {
      name: 'viewChoices',
      view: function(_, X) {
        return foam.u2.view.ChoiceView.create({
          choices: X.data.views
        });
      }
    },
  ],

  axioms: [
    foam.u2.CSS.create({
      code: function CSS() {/*
        ^ {
          width: 992px;
          margin: auto;
        }
        ^ .foam-u2-tag-Select{
          width: 100px;
          height: 40px;
        }
        ^ .property-viewChoices{
          margin: 0 0 25px 15px;
        }
      */}
    })
  ],

  methods: [
    function initE() {
      this.SUPER();
      var self = this;
      this.altStack = null;
      this.altStack = this.Stack.create();
      this.viewChoices$.sub(this.changeView);

      this.addClass(this.myClass())
      this.startContext({data: this})
        this.start()
          .add(this.VIEW_CHOICES)
        .end()
      .endContext()
      .start('div').addClass('stack-wrapper')
        .tag({class: 'foam.u2.stack.StackView', data: this.altStack, showActions: false})
      .end()

      if(!this.views[0]){
        this.views.push([ { class: 'foam.u2.view.TableView' , data: this.data }, 'Table' ]);
      } else {
        this.views.forEach(function(view){
          view[0].data ? view[0].data : self.data;
        });
        this.altStack.push(this.views[0][0]);
      }
    }
  ],

  listeners: [
    function changeView(){
      this.altStack.push(this.viewChoices);
    }
  ]
});<|endoftext|>


<commit_before>// LIFX - the smart wifi light bulb -- http://lifx.co

var lifx        = require('lifx')
  , util        = require('util')
  , tinycolor   = require('tinycolor2')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , broker      = utility.broker
  , lighting    = require('./../device-lighting')
  ;

var logger = lighting.logger;

var LIFX = exports.Device = function(deviceID, deviceUID, info) {
  var self = this;

  self.whatami = info.deviceType;
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName();

  self.status = 'waiting';
  self.changed();
  self.lx = info.lx;
  self.bulb = info.bulb;
  self.info = {};

  broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return self.perform(self, taskID, perform, parameter);
  });
};
util.inherits(LIFX, lighting.Device);

// the low-level driver calls this function when its state changes.
// possibly somebody else is changing the state of the hardware directly,
// or the hardware has built-in controls, like a light switch.

/*
>>> bulbstate
{ bulb: { lifxAddress: <Buffer d0 73 d5 00 b0 05>, name: '' },
  state:
   { hue: 0,
     saturation: 0,
     brightness: 65535,
     kelvin: 3500,
     dim: 0,
     power: 65535,
     bulbLabel: '',
     tags: <Buffer 00 00 00 00 00 00 00 00> } }
 */
LIFX.prototype.update = function(self, state) {
  self.status = state.power ? 'on' : 'off';
  if ((state.kelvin > 0) || (state.hue > 0) || (state.saturation > 0)) {
    if ((state.kelvin > 0) && (state.hue === 0) && (state.saturation === 0)) {
      self.info.color = { model       : 'temperature'
                        , temperature : { temperature : Math.round(1000000 / state.kelvin) }
                        };
    } else {
      self.info.color = { model       : 'rgb'
                        , rgb         : tinycolor({ h : devices.degreesValue(state.hue, 65535)
                                                  , s : devices.percentageValue(state.saturation, 65535) / 100
                                                  , l : devices.percentageValue(state.brightness, 65535) / 100
                                                  }).toRgb()
                        };
    }
  }
  if (!!state.brightness) self.info.brightness = devices.percentageValue(state.brightness, 65535);
  if ((!!state.bulblabel) && (state.bulblabel !== '')) self.setName(state.bulbLabel);

  self.changed();
};

// handle the calls from the steward to change things.
// set: set the internal name of the bulb.
// off: turn the bulb off.
// on: turn the bulb on and set its bulb to the 'color' and 'brightness' parameter.
LIFX.prototype.perform = function(self, taskID, perform, parameter) {
  var color, params, state, tc;

  try { params = JSON.parse(parameter); } catch(ex) { params = {}; }

  if (perform === 'set') return self.setName(params.name, taskID);

  state = {};
  if (perform === 'off') state.on = false;
  else if (perform !== 'on') return false;
  else {
    state.on = true;

    if ((!!params.brightness) && (lighting.validBrightness(params.brightness))) state.brightness = params.brightness;
    if (!state.brightness) state.brightness = self.info.brightness;

    state.color = params.color || self.info.color;
    if (!state.color) state.color = { model: 'rgb', rgb: { r: 255, g: 255, b: 255 } };
    switch (state.color.model) {
      case 'temperature':
        if (!state.color.temperature) return false;
        if (!lighting.validTemperature(state.color.temperature.temperature)) return false;
        break;

      case 'hue':
        if (!state.color.hue) return false;
        if ((!lighting.validHue(state.color.hue.hue)) || (!lighting.validSaturation(state.color.hue.saturation))) return false;
        break;

      case 'rgb':
        tc = tinycolor(state.color.rgb).toHsl();
        state.color = { model : 'hue', hue: { hue: Math.round(tc.h), saturation: devices.percentageValue(tc.s, 1) } };
        state.brightness = devices.percentageValue(tc.l, 1);
        break;

      default:
        break;
    }
  }

  logger.info('device/' + self.deviceID, { perform: state });

  if (!state.on) self.lx.lightsOff(self.bulb);
  else {
    if (self.status === 'off') self.lx.lightsOn(self.bulb);

    color = { hue: 0, saturation: 0, luminance: 0, whiteColor: 0, fadeTime: 0 };

    if (state.color.model === 'hue') {
      color.hue = devices.scaledPercentage((state.color.hue.hue / 360) * 100,  0, 65535);
      color.saturation = devices.scaledPercentage(state.color.hue.saturation, 0, 65535);
    } else color.whiteColor = 1000000 / state.color.temperature.temperature;
    color.luminance = devices.scaledPercentage(state.brightness, 0, 65535);

    self.lx.lightsColour(color.hue, color.saturation, color.luminance, color.whiteColor, color.fadeTime);

    if (state.color.model === 'hue') {
      state.color = { model       : 'rgb'
                    , rgb         : tinycolor({ h : state.color.hue.hue
                                              , s : state.color.hue.saturation / 100
                                              , l : state.brightness           / 100
                                              }).toRgb()
                    };
      delete(state.color.rgb.a);
    }
  }

// TBD: determine why not receiving update events... (if we can't fix this, we'll need to start polling...)
  self.status = state.on ? 'on' : 'off';
  if (state.on) {
    self.info.color = state.color;
    self.info.brightness = state.brightness;
  }
  self.changed();

  return steward.performed(taskID);
};

// check the parameters on the perform before we even try to do it.
var validate_perform = function(perform, parameter) {
  var color
    , params = {}
    , result = { invalid: [], requires: [] };

  if (!!parameter) try { params = JSON.parse(parameter); } catch(ex) { result.invalid.push('parameter'); }

  if (perform === 'off') return result;

  if (perform === 'set') {
    if (!params.name) result.requires.push('name');
    return result;
  }

  if (perform !== 'on') {
    result.invalid.push('perform');
    return result;
  }

  color = params.color;
  if (!!color) {
    switch (color.model) {
        case 'temperature':
          if (!color.temperature) { result.requires.push('color.temperature'); break; }
          if (!lighting.validTemperature(color.temperature.temperature)) result.invalid.push('color.temperature.temperature');
          break;

        case 'hue':
          if (!color.hue) { result.requires.push('color.hue'); break; }
          if (!lighting.validHue(color.hue.hue)) result.invalid.push('color.hue.hue');
          if (!lighting.validSaturation(color.hue.saturation)) result.invalid.push('color.hue.saturation');
          if (!params.brightness) result.requires.push('brightness');
          break;

        case 'rgb':
          if (!lighting.validRGB(color.rgb)) result.invalid.push('color.rgb');
          break;

        default:
          result.invalid.push('color.model');
          break;
    }
  }

  if ((!!params.brightness) && (!lighting.validBrightness(params.brightness))) result.invalid.push('brightness');

  return result;
};

exports.start = function() {
  var lx;

  steward.actors.device.lighting.LIFX = steward.actors.device.lighting.LIFX ||
      { $info     : { type: '/device/lighting/LIFX' } };

  steward.actors.device.lighting.LIFX.bulb =
      { $info     : { type       : '/device/lighting/LIFX/bulb'
                    , observe    : [ ]
                    , perform    : [ 'off', 'on' ]
                    , properties : { name       : true
                                   , status     : [ 'waiting', 'on', 'off' ]
                                   , color      : { model: [ { temperature : { temperature: 'mireds' } }
                                                           , { hue         : { hue: 'degrees', saturation: 'percentage' } }
                                                           , { rgb         : { r: 'u8', g: 'u8', b: 'u8' } }
                                                           ]
                                                  }
                                   , brightness : 'percentage'
                                   }
                    }
      , $validate : { perform    : validate_perform }
      };
  devices.makers['/device/lighting/LIFX/bulb'] = LIFX;

  lx = lifx.init();
  lx.on('bulb', function(bulb) {
    var info;

    info = { source     : 'LIFX'
           , lx         : lx
           , bulb       : bulb
           , device     : { url          : null
                          , name         : bulb.name
                          , manufacturer : "LIFX"
                          , model        : { name        : bulb.name
                                           , description : ''
                                           , number      : ''
                                           }
                          , unit         : { serial      : bulb.lifxAddress.toString('hex')
                                           , udn         : 'LIFX:' + bulb.lifxAddress.toString('hex')
                                           }
                          }
           };

    info.url = info.device.url;
    info.deviceType = '/device/lighting/LIFX/bulb';
    info.id = info.device.unit.udn;
    if (!!devices.devices[info.id]) return;

    devices.discover(info);
  });

// the LIFX stream is faster than the database on startup
  var update = function(udn, data) {
    var device;

    if ((!devices.devices[udn]) || (!devices.devices[udn].device)) return setTimeout(function() { update(udn, data); }, 10);

    device = devices.devices[udn].device;
    if (!!device) device.update(device, data);
  };

  lx.on('bulbstate', function(bulbstate) {
console.log('>>> bulbstate');  console.log(util.inspect(bulbstate, { depth: null }));
    var device,  udn;

    udn = 'LIFX:' + bulbstate.bulb.lifxAddress.toString('hex');
    if (!devices.devices[udn]) return update(udn, bulbstate.state);

    device = devices.devices[udn].device;
    if (!device) return update(udn, bulbstate.state);
    device.update(device, bulbstate.state);
  });

  lx.on('bulbonoff', function(bulbonoff) {
console.log('>>> bulbonoff');  console.log(util.inspect(bulbonoff, { depth: null }));
    var device, udn;

    udn = 'LIFX:' + bulbonoff.bulb.lifxAddress.toString('hex');
    if (!devices.devices[udn]) return update(udn, { power: bulbonoff.on ? 65535 : 0 });

    device = devices.devices[udn].device;
    if (!!device) device.update(device, { power: bulbonoff.on ? 65535 : 0 });
  });
};
<commit_msg>add background scanning, since the gateway doesn't refresh us...<commit_after>// LIFX - the smart wifi light bulb -- http://lifx.co

var lifx        = require('lifx')
  , util        = require('util')
  , tinycolor   = require('tinycolor2')
  , devices     = require('./../../core/device')
  , steward     = require('./../../core/steward')
  , utility     = require('./../../core/utility')
  , broker      = utility.broker
  , lighting    = require('./../device-lighting')
  ;

var logger = lighting.logger;

var LIFX = exports.Device = function(deviceID, deviceUID, info) {
  var self = this;

  self.whatami = info.deviceType;
  self.deviceID = deviceID.toString();
  self.deviceUID = deviceUID;
  self.name = info.device.name;
  self.getName();

  self.status = 'waiting';
  self.changed();
  self.lx = info.lx;
  self.bulb = info.bulb;
  self.info = {};

  broker.subscribe('actors', function(request, taskID, actor, perform, parameter) {
    if (actor !== ('device/' + self.deviceID)) return;

    if (request === 'perform') return self.perform(self, taskID, perform, parameter);
  });
};
util.inherits(LIFX, lighting.Device);

// the low-level driver calls this function when its state changes.
// possibly somebody else is changing the state of the hardware directly,
// or the hardware has built-in controls, like a light switch.

/*
>>> bulbstate
{ bulb: { lifxAddress: <Buffer d0 73 d5 00 b0 05>, name: '' },
  state:
   { hue: 0,
     saturation: 0,
     brightness: 65535,
     kelvin: 3500,
     dim: 0,
     power: 65535,
     bulbLabel: '',
     tags: <Buffer 00 00 00 00 00 00 00 00> } }
 */
LIFX.prototype.update = function(self, state) {
  logger.info('device/' + self.deviceID, { update: state });

  self.status = state.power ? 'on' : 'off';
  if ((state.kelvin > 0) || (state.hue > 0) || (state.saturation > 0)) {
    if ((state.kelvin > 0) && (state.hue === 0) && (state.saturation === 0)) {
      self.info.color = { model       : 'temperature'
                        , temperature : { temperature : Math.round(1000000 / state.kelvin) }
                        };
    } else {
      self.info.color = { model       : 'rgb'
                        , rgb         : tinycolor({ h : devices.degreesValue(state.hue, 65535)
                                                  , s : devices.percentageValue(state.saturation, 65535) / 100
                                                  , l : devices.percentageValue(state.brightness, 65535) / 100
                                                  }).toRgb()
                        };
    }
  }
  if (!!state.brightness) self.info.brightness = devices.percentageValue(state.brightness, 65535);
  if ((!!state.bulblabel) && (state.bulblabel !== '')) self.setName(state.bulbLabel);

  self.changed();
};

// handle the calls from the steward to change things.
// set: set the internal name of the bulb.
// off: turn the bulb off.
// on: turn the bulb on and set its bulb to the 'color' and 'brightness' parameter.
LIFX.prototype.perform = function(self, taskID, perform, parameter) {
  var color, params, state, tc;

  try { params = JSON.parse(parameter); } catch(ex) { params = {}; }

  if (perform === 'set') return self.setName(params.name, taskID);

  state = {};
  if (perform === 'off') state.on = false;
  else if (perform !== 'on') return false;
  else {
    state.on = true;

    if ((!!params.brightness) && (lighting.validBrightness(params.brightness))) state.brightness = params.brightness;
    if (!state.brightness) state.brightness = self.info.brightness;

    state.color = params.color || self.info.color;
    if (!state.color) state.color = { model: 'rgb', rgb: { r: 255, g: 255, b: 255 } };
    switch (state.color.model) {
      case 'temperature':
        if (!state.color.temperature) return false;
        if (!lighting.validTemperature(state.color.temperature.temperature)) return false;
        break;

      case 'hue':
        if (!state.color.hue) return false;
        if ((!lighting.validHue(state.color.hue.hue)) || (!lighting.validSaturation(state.color.hue.saturation))) return false;
        break;

      case 'rgb':
        tc = tinycolor(state.color.rgb).toHsl();
        state.color = { model : 'hue', hue: { hue: Math.round(tc.h), saturation: devices.percentageValue(tc.s, 1) } };
        state.brightness = devices.percentageValue(tc.l, 1);
        break;

      default:
        break;
    }
  }

  logger.info('device/' + self.deviceID, { perform: state });

  if (!state.on) self.lx.lightsOff(self.bulb);
  else {
    if (self.status === 'off') self.lx.lightsOn(self.bulb);

    color = { hue: 0, saturation: 0, luminance: 0, whiteColor: 0, fadeTime: 0 };

    if (state.color.model === 'hue') {
      color.hue = devices.scaledPercentage((state.color.hue.hue / 360) * 100,  0, 65535);
      color.saturation = devices.scaledPercentage(state.color.hue.saturation, 0, 65535);
    } else color.whiteColor = 1000000 / state.color.temperature.temperature;
    color.luminance = devices.scaledPercentage(state.brightness, 0, 65535);

    self.lx.lightsColour(color.hue, color.saturation, color.luminance, color.whiteColor, color.fadeTime);

    if (state.color.model === 'hue') {
      state.color = { model       : 'rgb'
                    , rgb         : tinycolor({ h : state.color.hue.hue
                                              , s : state.color.hue.saturation / 100
                                              , l : state.brightness           / 100
                                              }).toRgb()
                    };
      delete(state.color.rgb.a);
    }
  }

  self.status = state.on ? 'on' : 'off';
  if (state.on) {
    self.info.color = state.color;
    self.info.brightness = state.brightness;
  }
  self.changed();

  return steward.performed(taskID);
};

// check the parameters on the perform before we even try to do it.
var validate_perform = function(perform, parameter) {
  var color
    , params = {}
    , result = { invalid: [], requires: [] };

  if (!!parameter) try { params = JSON.parse(parameter); } catch(ex) { result.invalid.push('parameter'); }

  if (perform === 'off') return result;

  if (perform === 'set') {
    if (!params.name) result.requires.push('name');
    return result;
  }

  if (perform !== 'on') {
    result.invalid.push('perform');
    return result;
  }

  color = params.color;
  if (!!color) {
    switch (color.model) {
        case 'temperature':
          if (!color.temperature) { result.requires.push('color.temperature'); break; }
          if (!lighting.validTemperature(color.temperature.temperature)) result.invalid.push('color.temperature.temperature');
          break;

        case 'hue':
          if (!color.hue) { result.requires.push('color.hue'); break; }
          if (!lighting.validHue(color.hue.hue)) result.invalid.push('color.hue.hue');
          if (!lighting.validSaturation(color.hue.saturation)) result.invalid.push('color.hue.saturation');
          if (!params.brightness) result.requires.push('brightness');
          break;

        case 'rgb':
          if (!lighting.validRGB(color.rgb)) result.invalid.push('color.rgb');
          break;

        default:
          result.invalid.push('color.model');
          break;
    }
  }

  if ((!!params.brightness) && (!lighting.validBrightness(params.brightness))) result.invalid.push('brightness');

  return result;
};

exports.start = function() {
  var lx;

  steward.actors.device.lighting.LIFX = steward.actors.device.lighting.LIFX ||
      { $info     : { type: '/device/lighting/LIFX' } };

  steward.actors.device.lighting.LIFX.bulb =
      { $info     : { type       : '/device/lighting/LIFX/bulb'
                    , observe    : [ ]
                    , perform    : [ 'off', 'on' ]
                    , properties : { name       : true
                                   , status     : [ 'waiting', 'on', 'off' ]
                                   , color      : { model: [ { temperature : { temperature: 'mireds' } }
                                                           , { hue         : { hue: 'degrees', saturation: 'percentage' } }
                                                           , { rgb         : { r: 'u8', g: 'u8', b: 'u8' } }
                                                           ]
                                                  }
                                   , brightness : 'percentage'
                                   }
                    }
      , $validate : { perform    : validate_perform }
      };
  devices.makers['/device/lighting/LIFX/bulb'] = LIFX;

  lx = lifx.init();
  lx.on('bulb', function(bulb) {
    var info;

    info = { source     : 'LIFX'
           , lx         : lx
           , bulb       : bulb
           , device     : { url          : null
                          , name         : bulb.name
                          , manufacturer : "LIFX"
                          , model        : { name        : bulb.name
                                           , description : ''
                                           , number      : ''
                                           }
                          , unit         : { serial      : bulb.lifxAddress.toString('hex')
                                           , udn         : 'LIFX:' + bulb.lifxAddress.toString('hex')
                                           }
                          }
           };

    info.url = info.device.url;
    info.deviceType = '/device/lighting/LIFX/bulb';
    info.id = info.device.unit.udn;
    if (!!devices.devices[info.id]) return;

    devices.discover(info);
  });
  setInterval(function() { lx.findBulbs(); }, 30 * 1000);

// the LIFX stream is faster than the database on startup
  var update = function(udn, data) {
    var device;

    if ((!devices.devices[udn]) || (!devices.devices[udn].device)) return setTimeout(function() { update(udn, data); }, 10);

    device = devices.devices[udn].device;
    if (!!device) device.update(device, data);
  };

  lx.on('bulbstate', function(bulbstate) {
    var device,  udn;

    udn = 'LIFX:' + bulbstate.bulb.lifxAddress.toString('hex');
    if (!devices.devices[udn]) return update(udn, bulbstate.state);

    device = devices.devices[udn].device;
    if (!device) return update(udn, bulbstate.state);
    device.update(device, bulbstate.state);
  });

  lx.on('bulbonoff', function(bulbonoff) {
    var device, onoff, udn;

    onoff = { power: bulbonoff.on ? 65535 : 0 };

    udn = 'LIFX:' + bulbonoff.bulb.lifxAddress.toString('hex');
     if (!devices.devices[udn]) return update(udn, onoff);

    device = devices.devices[udn].device;
    if (!device) return update(udn, onoff);
    device.update(device, onoff);
  });
};
<|endoftext|>


<commit_before>window.botamp = (function() {
  var api, api_key, page_id, promise, api_base = 'https://app.botamp.com/api/v1/';

  window.onclick = function(e) {
    href = e.target.getAttribute('href')

    if (!(e.target.tagName.toLowerCase() == 'a' && href.includes('m.me/')))
      return;

    var saved_id = saved_contact_id();
    if (saved_id == null)
      return;

    e.preventDefault();
    document.location.href = href + '?ref=' + encodeURIComponent('botamp?btp_cid=' + saved_id);
  }

  var Botamp = function Botamp() {}

  function api_url(resource, id, sub_resource) {
    url = api_base + resource;
    if (id != undefined)
      url += ('/' + id);
    if (sub_resource != undefined)
      url += ('/' + sub_resource)
    return url;
  }

  function set_request_headers() {
    api.setRequestHeader('Content-Type', 'application/vnd.api+json');
    api.setRequestHeader('Authorization', "Basic " + btoa(api_key + ':'));
    api.withCredentials = true;
  }

  function request_body(resource, attributes) {
    body = {
      'data': {
        'type': resource,
        'attributes': attributes,
      }
    }

    return JSON.stringify(body);
  }

  function contact_id_key() {
    return 'botamp_' + page_id + '_contact_id';
  }

  function saved_contact_id() {
    return localStorage.getItem(contact_id_key());
  }

  function create_contact(attributes) {
    promise.then(function() {
      api.open('POST', api_url('contacts'));
      set_request_headers();
      api.onload = function() {
        if(api.status === 201) {
          localStorage.setItem(contact_id_key(), JSON.parse(api.responseText)['data']['id'])
        }
      }
      api.send(request_body('contacts', attributes))
    })
  }

  function update_contact(id, attributes) {
    promise.then(function(){
      api.open('PUT', api_url('contacts', id));
      set_request_headers();
      api.send(request_body('contacts', attributes))
    })
  }

  Botamp.prototype.load = function(public_api_key) {
    api_key = public_api_key;

    api = new XMLHttpRequest();

    promise = new Promise(function(resolve, reject){
      api.open('GET', api_url('me'), true);
      api.onload = function() {
        if(api.status == 200) {
          page_id = JSON.parse(api.responseText)['data']['id'];

          match_contact_id = window.location.href.match(/btp_cid=(\d+)/);
          if (match_contact_id != null) {
            api.open('GET', api_url('contacts',match_contact_id[1]), true);
            api.onload = function() {
              if(api.status == 200)
                localStorage.setItem(contact_id_key(), match_contact_id[1]);
            }
            set_request_headers();
            api.send();
          }

          resolve();
        }
        else {
          reject();
        }
      }

      api.onerror = function() {
        reject();
      }
      set_request_headers();
      api.send();
    })

    promise.catch(function(error) {
      throw error;
    })
  }

  Botamp.prototype.identify = function() {
    if (arguments.length == 1) {
      var saved_id = saved_contact_id();
      if (saved_id == null)
        create_contact(arguments[0])
      else
        update_contact(saved_id, arguments[0])
    } else if (arguments.length == 2) {
      update_contact(arguments[0], arguments[1])
    }
  }

  Botamp.prototype.track = function(name, properties) {
    var attributes = {};
    if (properties != undefined)
      attributes = properties;
    attributes['name'] = name;

    promise.then(function() {
      api.open('POST', api_url('contacts', saved_contact_id(), 'events'));
      set_request_headers();
      api.send(request_body('events', attributes))
    })
  }

  return new Botamp();
}());
<commit_msg>Fix attributes object construction<commit_after>window.botamp = (function() {
  var api, api_key, page_id, promise, api_base = 'https://app.botamp.com/api/v1/';

  window.onclick = function(e) {
    href = e.target.getAttribute('href')

    if (!(e.target.tagName.toLowerCase() == 'a' && href.includes('m.me/')))
      return;

    var saved_id = saved_contact_id();
    if (saved_id == null)
      return;

    e.preventDefault();
    document.location.href = href + '?ref=' + encodeURIComponent('botamp?btp_cid=' + saved_id);
  }

  var Botamp = function Botamp() {}

  function api_url(resource, id, sub_resource) {
    url = api_base + resource;
    if (id != undefined)
      url += ('/' + id);
    if (sub_resource != undefined)
      url += ('/' + sub_resource)
    return url;
  }

  function set_request_headers() {
    api.setRequestHeader('Content-Type', 'application/vnd.api+json');
    api.setRequestHeader('Authorization', "Basic " + btoa(api_key + ':'));
    api.withCredentials = true;
  }

  function request_body(resource, attributes) {
    body = {
      'data': {
        'type': resource,
        'attributes': attributes,
      }
    }

    return JSON.stringify(body);
  }

  function contact_id_key() {
    return 'botamp_' + page_id + '_contact_id';
  }

  function saved_contact_id() {
    return localStorage.getItem(contact_id_key());
  }

  function create_contact(attributes) {
    promise.then(function() {
      api.open('POST', api_url('contacts'));
      set_request_headers();
      api.onload = function() {
        if(api.status === 201) {
          localStorage.setItem(contact_id_key(), JSON.parse(api.responseText)['data']['id'])
        }
      }
      api.send(request_body('contacts', attributes))
    })
  }

  function update_contact(id, attributes) {
    promise.then(function(){
      api.open('PUT', api_url('contacts', id));
      set_request_headers();
      api.send(request_body('contacts', attributes))
    })
  }

  Botamp.prototype.load = function(public_api_key) {
    api_key = public_api_key;

    api = new XMLHttpRequest();

    promise = new Promise(function(resolve, reject){
      api.open('GET', api_url('me'), true);
      api.onload = function() {
        if(api.status == 200) {
          page_id = JSON.parse(api.responseText)['data']['id'];

          match_contact_id = window.location.href.match(/btp_cid=(\d+)/);
          if (match_contact_id != null) {
            api.open('GET', api_url('contacts',match_contact_id[1]), true);
            api.onload = function() {
              if(api.status == 200)
                localStorage.setItem(contact_id_key(), match_contact_id[1]);
            }
            set_request_headers();
            api.send();
          }

          resolve();
        }
        else {
          reject();
        }
      }

      api.onerror = function() {
        reject();
      }
      set_request_headers();
      api.send();
    })

    promise.catch(function(error) {
      throw error;
    })
  }

  Botamp.prototype.identify = function() {
    if (arguments.length == 1) {
      var saved_id = saved_contact_id();
      if (saved_id == null)
        create_contact(arguments[0])
      else
        update_contact(saved_id, arguments[0])
    } else if (arguments.length == 2) {
      update_contact(arguments[0], arguments[1])
    }
  }

  Botamp.prototype.track = function(name, properties) {
    var attributes = {name: name};
    if (properties != undefined)
      attributes.properties = properties;

    promise.then(function() {
      api.open('POST', api_url('contacts', saved_contact_id(), 'events'));
      set_request_headers();
      api.send(request_body('events', attributes))
    })
  }

  return new Botamp();
}());
<|endoftext|>


<commit_before>/**
 * @file guildMemberRemove event
 * @author Sankarsan Kampa (a.k.a k3rn31p4nic)
 * @license MIT
 */

module.exports = async member => {
  // Commented this out as using requires BAN_MEMBERS perms and not everyone would've given the bot perms
  // member.guild.fetchBans().then(users => {if (users.has(member.id)) return;}).catch(e => {member.client.log(e);})
  member.client.emit('serverLog', member.client, member.guild, 'guildMemberRemove', {
    member: member
  });

  try {
    let guild = await member.client.db.get(`SELECT farewell, farewellMessage, farewellTimeout FROM guildSettings WHERE guildID=${member.guild.id}`);
    if (!guild) return;

    if (guild.farewell) {
      let farewellMessage = 'May we meet again.';
      if (guild.farewellMessage) {
        farewellMessage = await member.client.functions.decodeString(guild.farewellMessage);
      }
      farewellMessage = farewellMessage.replace(/\$user/ig, `<@${member.id}>`);
      farewellMessage = farewellMessage.replace(/\$server/ig, member.guild.name);
      farewellMessage = farewellMessage.replace(/\$username/ig, member.displayName);
      farewellMessage = farewellMessage.replace(/\$prefix/ig, member.guild.prefix[0] || member.client.config.prefix);

      member.guild.channels.get(guild.farewell).send({
        embed: {
          color: member.client.colors.BLUE,
          title: `Goodbye ${member.displayName}!`,
          description: farewellMessage
        }
      }).then(m => {
        if (guild.farewellTimeout > 0) {
          m.delete(1000 * parseInt(guild.farewellTimeout)).catch(e => {
            member.client.log.error(e);
          });
        }
      }).catch(e => {
        member.client.log.error(e);
      });
    }
  }
  catch (e) {
    member.client.log.error(e);
  }
};
<commit_msg>Added guildMemberRemove log code to event listner<commit_after>/**
 * @file guildMemberRemove event
 * @author Sankarsan Kampa (a.k.a k3rn31p4nic)
 * @license MIT
 */

module.exports = async member => {
  // Commented this out as using requires BAN_MEMBERS perms and not everyone would've given the bot perms
  // member.guild.fetchBans().then(users => {if (users.has(member.id)) return;}).catch(e => {member.client.log(e);})

  try {
    let guild = await member.client.db.get(`SELECT farewell, farewellMessage, farewellTimeout, log FROM guildSettings WHERE guildID=${member.guild.id}`);
    if (!guild) return;

    if (guild.log) {
      let logChannel = member.guild.channels.get(guild.log);
      if (logChannel) {
        logChannel.send({
          embed: {
            color: member.guild.client.colors.RED,
            title: 'User Left',
            fields: [
              {
                name: 'User',
                value: member.user.tag,
                inline: true
              },
              {
                name: 'User ID',
                value: member.id,
                inline: true
              }
            ],
            timestamp: new Date()
          }
        }).catch(e => {
          member.guild.client.log.error(e);
        });
      }
    }

    if (guild.farewell) {
      let farewellMessage = 'May we meet again.';
      if (guild.farewellMessage) {
        farewellMessage = await member.client.functions.decodeString(guild.farewellMessage);
      }
      farewellMessage = farewellMessage.replace(/\$user/ig, `<@${member.id}>`);
      farewellMessage = farewellMessage.replace(/\$server/ig, member.guild.name);
      farewellMessage = farewellMessage.replace(/\$username/ig, member.displayName);
      farewellMessage = farewellMessage.replace(/\$prefix/ig, member.guild.prefix[0] || member.client.config.prefix);

      member.guild.channels.get(guild.farewell).send({
        embed: {
          color: member.client.colors.BLUE,
          title: `Goodbye ${member.displayName}!`,
          description: farewellMessage
        }
      }).then(m => {
        if (guild.farewellTimeout > 0) {
          m.delete(1000 * parseInt(guild.farewellTimeout)).catch(e => {
            member.client.log.error(e);
          });
        }
      }).catch(e => {
        member.client.log.error(e);
      });
    }
  }
  catch (e) {
    member.client.log.error(e);
  }
};
<|endoftext|>


<commit_before>
// Displays an error message to the UI. Any previous message will be erased.
function displayError(message) {
  console.error(message);
}

// Creates a generator that reads the response body one line at a time.
async function* bodyByLinesIterator(response, updateProgress) {
  const utf8Decoder = new TextDecoder('utf-8');
  const reader = response.body.getReader();

  const re = /\n|\r|\r\n/gm;
  let pendingText = "";

  let readerDone = false;
  while (!readerDone) {
    // Read a chunk.
    const { value: chunk, done } = await reader.read();
    readerDone = done;
    if (!chunk) {
      continue;
    }
    // Notify the listener of progress.
    updateProgress(chunk.length);
    const decodedChunk = utf8Decoder.decode(chunk);

    let startIndex = 0;
    let result;
    // Keep processing until there are no more new lines.
    while ((result = re.exec(decodedChunk)) !== null) {
      const text = decodedChunk.substring(startIndex, result.index);
      startIndex = re.lastIndex;

      const line = pendingText + text;
      pendingText = "";
      if (line !== "") {
        yield line;
      }
    }
    // Any text after the last new line is appended to any pending text.
    pendingText += decodedChunk.substring(startIndex);
  }

  // If there is any text remaining, return it.
  if (pendingText !== "") {
    yield pendingText;
  }
}

// Determines whether `str` matches at least one value in `enumObject`.
function isValidEnumValue(enumObject, str) {
  for (const enumKey in enumObject) {
    if (enumObject[enumKey] === str) {
      return true;
    }
  }
  return false;
}

// Enum for test status.
const testStatus = {
  PASSED: "Passed",
  FAILED: "Failed",
  SKIPPED: "Skipped"
}

async function loadTestData() {
  const response = await fetch("data.csv");
  if (!response.ok) {
    const responseText = await response.text();
    throw `Failed to fetch data from GCS bucket. Error: ${responseText}`;
  }

  const box = document.createElement("div");
  box.style.width = "100%";
  const innerBox = document.createElement("div");
  innerBox.style.margin = "5rem";
  box.appendChild(innerBox);
  const progressBarPrompt = document.createElement("h1");
  progressBarPrompt.style.fontFamily = "Arial";
  progressBarPrompt.style.textAlign = "center";
  progressBarPrompt.innerText = "Downloading data...";
  innerBox.appendChild(progressBarPrompt);
  const progressBar = document.createElement("progress");
  progressBar.setAttribute("max", Number(response.headers.get('Content-Length')));
  progressBar.style.width = "100%";
  innerBox.appendChild(progressBar);
  document.body.appendChild(box);

  let readBytes = 0;
  const lines = bodyByLinesIterator(response, value => {
    readBytes += value;
    progressBar.setAttribute("value", readBytes);
  });
  // Consume the header to ensure the data has the right number of fields.
  const header = (await lines.next()).value;
  if (header.split(",").length != 6) {
    document.body.removeChild(box);
    throw `Fetched CSV data contains wrong number of fields. Expected: 6. Actual Header: "${header}"`;
  }

  const testData = [];
  let lineData = ["", "", "", "", "", ""];
  for await (const line of lines) {
    let splitLine = line.split(",");
    if (splitLine.length != 6) {
      console.warn(`Found line with wrong number of fields. Actual: ${splitLine.length} Expected: 6. Line: "${line}"`);
      continue;
    }
    splitLine = splitLine.map((value, index) => value === "" ? lineData[index] : value);
    lineData = splitLine;
    if (!isValidEnumValue(testStatus, splitLine[4])) {
      console.warn(`Invalid test status provided. Actual: ${splitLine[4]} Expected: One of ${Object.values(testStatus).join(", ")}`);
      continue;
    }
    testData.push({
      commit: splitLine[0],
      date: new Date(splitLine[1]),
      environment: splitLine[2],
      name: splitLine[3],
      status: splitLine[4],
      duration: Number(splitLine[5]),
    });
  }
  document.body.removeChild(box);
  if (testData.length == 0) {
    throw "Fetched CSV data is empty or poorly formatted.";
  }
  return testData;
}

Array.prototype.sum = function() {
  return this.reduce((sum, value) => sum + value, 0);
};

// Computes the average of an array of numbers.
Array.prototype.average = function () {
  return this.length === 0 ? 0 : (this.sum() / this.length);
};

// Groups array elements by keys obtained through `keyGetter`.
Array.prototype.groupBy = function (keyGetter) {
  return Array.from(this.reduce((mapCollection, element) => {
    const key = keyGetter(element);
    if (mapCollection.has(key)) {
      mapCollection.get(key).push(element);
    } else {
      mapCollection.set(key, [element]);
    }
    return mapCollection;
  }, new Map()).values());
};

// Parse URL search `query` into [{key, value}].
function parseUrlQuery(query) {
  if (query[0] === '?') {
    query = query.substring(1);
  }
  return Object.fromEntries((query === "" ? [] : query.split("&")).map(element => {
    const keyValue = element.split("=");
    return [unescape(keyValue[0]), unescape(keyValue[1])];
  }));
}

async function init() {
  google.charts.load('current', { 'packages': ['corechart'] });
  let testData;
  try {
    // Wait for Google Charts to load, and for test data to load.
    // Only store the test data (at index 1) into `testData`.
    testData = (await Promise.all([
      new Promise(resolve => google.charts.setOnLoadCallback(resolve)),
      loadTestData()
    ]))[1];
  } catch (err) {
    displayError(err);
    return;
  }

  const data = new google.visualization.DataTable();
  data.addColumn('date', 'Date');
  data.addColumn('number', 'Flake Percentage');
  data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
  data.addColumn('number', 'Duration');
  data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });

  const query = parseUrlQuery(window.location.search);
  const desiredTest = query.test || "", desiredEnvironment = query.env || "";

  const groups = testData
    // Filter to only contain unskipped runs of the requested test and requested environment.
    .filter(test => test.name === desiredTest && test.environment === desiredEnvironment && test.status !== testStatus.SKIPPED)
    .groupBy(test => test.date.getTime());

  const hashToLink = (hash, environment) => `https://storage.googleapis.com/minikube-builds/logs/master/${hash.substring(0,7)}/${environment}.html`;

  data.addRows(
    groups
      // Sort by run date, past to future.
      .sort((a, b) => a[0].date - b[0].date)
      // Map each group to all variables need to format the rows.
      .map(tests => ({
        date: tests[0].date, // Get one of the dates from the tests (which will all be the same).
        flakeRate: tests.map(test => test.status === testStatus.FAILED ? 100 : 0).average(), // Compute average of runs where FAILED counts as 100%.
        duration: tests.map(test => test.duration).average(), // Compute average duration of runs.
        commitHashes: tests.map(test => ({ // Take all hashes, statuses, and durations of tests in this group.
          hash: test.commit,
          status: test.status,
          duration: test.duration
        })).groupBy(run => run.hash).map(runsWithSameHash => ({
          hash: runsWithSameHash[0].hash,
          failures: runsWithSameHash.map(run => run.status === testStatus.FAILED ? 1 : 0).sum(),
          runs: runsWithSameHash.length,
          duration: runsWithSameHash.map(run => run.duration).average(),
        }))
      }))
      .map(groupData => [
        groupData.date,
        groupData.flakeRate,
        `

          ${groupData.date.toString()}

          Flake Percentage: ${groupData.flakeRate.toFixed(2)}%

          Hashes:

          ${groupData.commitHashes.map(({ hash, failures, runs }) => `  - <a href="${hashToLink(hash, desiredEnvironment)}">${hash}</a> (Failures: ${failures}/${runs})`).join("
")}

`,
        groupData.duration,
        `

          ${groupData.date.toString()}

          Average Duration: ${groupData.duration.toFixed(2)}s

          Hashes:

          ${groupData.commitHashes.map(({ hash, runs, duration }) => `  - <a href="${hashToLink(hash, desiredEnvironment)}">${hash}</a> (Average of ${runs}: ${duration.toFixed(2)}s)`).join("
")}

`,
      ])
  );

  const options = {
    title: `Flake rate and duration by day of ${desiredTest} on ${desiredEnvironment}`,
    width: window.innerWidth,
    height: window.innerHeight,
    pointSize: 10,
    pointShape: "circle",
    series: {
      0: { targetAxisIndex: 0 },
      1: { targetAxisIndex: 1 },
    },
    vAxes: {
      0: { title: "Flake rate", minValue: 0, maxValue: 100 },
      1: { title: "Duration (seconds)" },
    },
    colors: ['#dc3912', '#3366cc'],
    tooltip: { trigger: "selection", isHtml: true }
  };
  const chart = new google.visualization.LineChart(document.getElementById('chart_div'));
  chart.draw(data, options);
}

init();
<commit_msg>Improve error handling to actually render in the UI.<commit_after>
// Displays an error message to the UI. Any previous message will be erased.
function displayError(message) {
  // Clear the body of all children.
  while (document.body.firstChild) {
    document.body.removeChild(document.body.firstChild);
  }
  const element = document.createElement("p");
  element.innerText = "Error: " + message;
  element.style.color = "red";
  element.style.fontFamily = "Arial";
  element.style.fontWeight = "bold";
  element.style.margin = "5rem";
  document.body.appendChild(element);
}

// Creates a generator that reads the response body one line at a time.
async function* bodyByLinesIterator(response, updateProgress) {
  const utf8Decoder = new TextDecoder('utf-8');
  const reader = response.body.getReader();

  const re = /\n|\r|\r\n/gm;
  let pendingText = "";

  let readerDone = false;
  while (!readerDone) {
    // Read a chunk.
    const { value: chunk, done } = await reader.read();
    readerDone = done;
    if (!chunk) {
      continue;
    }
    // Notify the listener of progress.
    updateProgress(chunk.length);
    const decodedChunk = utf8Decoder.decode(chunk);

    let startIndex = 0;
    let result;
    // Keep processing until there are no more new lines.
    while ((result = re.exec(decodedChunk)) !== null) {
      const text = decodedChunk.substring(startIndex, result.index);
      startIndex = re.lastIndex;

      const line = pendingText + text;
      pendingText = "";
      if (line !== "") {
        yield line;
      }
    }
    // Any text after the last new line is appended to any pending text.
    pendingText += decodedChunk.substring(startIndex);
  }

  // If there is any text remaining, return it.
  if (pendingText !== "") {
    yield pendingText;
  }
}

// Determines whether `str` matches at least one value in `enumObject`.
function isValidEnumValue(enumObject, str) {
  for (const enumKey in enumObject) {
    if (enumObject[enumKey] === str) {
      return true;
    }
  }
  return false;
}

// Enum for test status.
const testStatus = {
  PASSED: "Passed",
  FAILED: "Failed",
  SKIPPED: "Skipped"
}

async function loadTestData() {
  const response = await fetch("data.csv");
  if (!response.ok) {
    const responseText = await response.text();
    throw `Failed to fetch data from GCS bucket. Error: ${responseText}`;
  }

  const box = document.createElement("div");
  box.style.width = "100%";
  const innerBox = document.createElement("div");
  innerBox.style.margin = "5rem";
  box.appendChild(innerBox);
  const progressBarPrompt = document.createElement("h1");
  progressBarPrompt.style.fontFamily = "Arial";
  progressBarPrompt.style.textAlign = "center";
  progressBarPrompt.innerText = "Downloading data...";
  innerBox.appendChild(progressBarPrompt);
  const progressBar = document.createElement("progress");
  progressBar.setAttribute("max", Number(response.headers.get('Content-Length')));
  progressBar.style.width = "100%";
  innerBox.appendChild(progressBar);
  document.body.appendChild(box);

  let readBytes = 0;
  const lines = bodyByLinesIterator(response, value => {
    readBytes += value;
    progressBar.setAttribute("value", readBytes);
  });
  // Consume the header to ensure the data has the right number of fields.
  const header = (await lines.next()).value;
  if (header.split(",").length != 6) {
    document.body.removeChild(box);
    throw `Fetched CSV data contains wrong number of fields. Expected: 6. Actual Header: "${header}"`;
  }

  const testData = [];
  let lineData = ["", "", "", "", "", ""];
  for await (const line of lines) {
    let splitLine = line.split(",");
    if (splitLine.length != 6) {
      console.warn(`Found line with wrong number of fields. Actual: ${splitLine.length} Expected: 6. Line: "${line}"`);
      continue;
    }
    splitLine = splitLine.map((value, index) => value === "" ? lineData[index] : value);
    lineData = splitLine;
    if (!isValidEnumValue(testStatus, splitLine[4])) {
      console.warn(`Invalid test status provided. Actual: ${splitLine[4]} Expected: One of ${Object.values(testStatus).join(", ")}`);
      continue;
    }
    testData.push({
      commit: splitLine[0],
      date: new Date(splitLine[1]),
      environment: splitLine[2],
      name: splitLine[3],
      status: splitLine[4],
      duration: Number(splitLine[5]),
    });
  }
  document.body.removeChild(box);
  if (testData.length == 0) {
    throw "Fetched CSV data is empty or poorly formatted.";
  }
  return testData;
}

Array.prototype.sum = function() {
  return this.reduce((sum, value) => sum + value, 0);
};

// Computes the average of an array of numbers.
Array.prototype.average = function () {
  return this.length === 0 ? 0 : (this.sum() / this.length);
};

// Groups array elements by keys obtained through `keyGetter`.
Array.prototype.groupBy = function (keyGetter) {
  return Array.from(this.reduce((mapCollection, element) => {
    const key = keyGetter(element);
    if (mapCollection.has(key)) {
      mapCollection.get(key).push(element);
    } else {
      mapCollection.set(key, [element]);
    }
    return mapCollection;
  }, new Map()).values());
};

// Parse URL search `query` into [{key, value}].
function parseUrlQuery(query) {
  if (query[0] === '?') {
    query = query.substring(1);
  }
  return Object.fromEntries((query === "" ? [] : query.split("&")).map(element => {
    const keyValue = element.split("=");
    return [unescape(keyValue[0]), unescape(keyValue[1])];
  }));
}

async function init() {
  google.charts.load('current', { 'packages': ['corechart'] });
  let testData;
  try {
    // Wait for Google Charts to load, and for test data to load.
    // Only store the test data (at index 1) into `testData`.
    testData = (await Promise.all([
      new Promise(resolve => google.charts.setOnLoadCallback(resolve)),
      loadTestData()
    ]))[1];
  } catch (err) {
    displayError(err);
    return;
  }

  const data = new google.visualization.DataTable();
  data.addColumn('date', 'Date');
  data.addColumn('number', 'Flake Percentage');
  data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });
  data.addColumn('number', 'Duration');
  data.addColumn({ type: 'string', role: 'tooltip', 'p': { 'html': true } });

  const query = parseUrlQuery(window.location.search);
  const desiredTest = query.test || "", desiredEnvironment = query.env || "";

  const groups = testData
    // Filter to only contain unskipped runs of the requested test and requested environment.
    .filter(test => test.name === desiredTest && test.environment === desiredEnvironment && test.status !== testStatus.SKIPPED)
    .groupBy(test => test.date.getTime());

  const hashToLink = (hash, environment) => `https://storage.googleapis.com/minikube-builds/logs/master/${hash.substring(0,7)}/${environment}.html`;

  data.addRows(
    groups
      // Sort by run date, past to future.
      .sort((a, b) => a[0].date - b[0].date)
      // Map each group to all variables need to format the rows.
      .map(tests => ({
        date: tests[0].date, // Get one of the dates from the tests (which will all be the same).
        flakeRate: tests.map(test => test.status === testStatus.FAILED ? 100 : 0).average(), // Compute average of runs where FAILED counts as 100%.
        duration: tests.map(test => test.duration).average(), // Compute average duration of runs.
        commitHashes: tests.map(test => ({ // Take all hashes, statuses, and durations of tests in this group.
          hash: test.commit,
          status: test.status,
          duration: test.duration
        })).groupBy(run => run.hash).map(runsWithSameHash => ({
          hash: runsWithSameHash[0].hash,
          failures: runsWithSameHash.map(run => run.status === testStatus.FAILED ? 1 : 0).sum(),
          runs: runsWithSameHash.length,
          duration: runsWithSameHash.map(run => run.duration).average(),
        }))
      }))
      .map(groupData => [
        groupData.date,
        groupData.flakeRate,
        `

          ${groupData.date.toString()}

          Flake Percentage: ${groupData.flakeRate.toFixed(2)}%

          Hashes:

          ${groupData.commitHashes.map(({ hash, failures, runs }) => `  - <a href="${hashToLink(hash, desiredEnvironment)}">${hash}</a> (Failures: ${failures}/${runs})`).join("
")}

`,
        groupData.duration,
        `

          ${groupData.date.toString()}

          Average Duration: ${groupData.duration.toFixed(2)}s

          Hashes:

          ${groupData.commitHashes.map(({ hash, runs, duration }) => `  - <a href="${hashToLink(hash, desiredEnvironment)}">${hash}</a> (Average of ${runs}: ${duration.toFixed(2)}s)`).join("
")}

`,
      ])
  );

  const options = {
    title: `Flake rate and duration by day of ${desiredTest} on ${desiredEnvironment}`,
    width: window.innerWidth,
    height: window.innerHeight,
    pointSize: 10,
    pointShape: "circle",
    series: {
      0: { targetAxisIndex: 0 },
      1: { targetAxisIndex: 1 },
    },
    vAxes: {
      0: { title: "Flake rate", minValue: 0, maxValue: 100 },
      1: { title: "Duration (seconds)" },
    },
    colors: ['#dc3912', '#3366cc'],
    tooltip: { trigger: "selection", isHtml: true }
  };
  const chart = new google.visualization.LineChart(document.getElementById('chart_div'));
  chart.draw(data, options);
}

init();
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { map, find } from 'lodash';
import { RestApiClient } from '@dosomething/gateway';

import Post from '../Post';
import Empty from '../Empty';
import FilterBar from '../FilterBar';
import HistoryModal from '../HistoryModal';
import PagingButtons from '../PagingButtons';
import StatusCounter from '../StatusCounter';
import DropdownFilter from '../DropdownFilter';
import ModalContainer from '../ModalContainer';
import MultiValueFilter from '../MultiValueFilter';
import UserExport from '../UserExport';

class CampaignSingle extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      loadingNewPosts: false,
      filters: {
        status: 'accepted',
        tags: {
          'good-submission': {
            label: 'Good Submission',
            active: false,
          },
          'good-quote': {
            label: 'Good Quote',
            active: false,
          },
          'hide-in-gallery': {
            label: 'Hide In Gallery 👻',
            active: false,
          },
          'group-photo': {
            label: 'Group Photo',
            active: false,
          },
          'good-for-sponsor': {
            label: 'Good For Sponsor',
            active: false,
          },
          'good-for-storytelling': {
            label: 'Good For Storytelling',
            active: false,
          },
          'good-for-brand': {
            label: 'Good For Brand',
            active: false,
          },
          bulk: {
            label: 'Bulk',
            active: false,
          },
        },
      },
    };

    this.api = new RestApiClient(window.location.origin, {
      headers: { Authorization: `Bearer ${window.AUTH}` },
    });
    this.filterPosts = this.filterPosts.bind(this);
    this.getPostsByFilter = this.getPostsByFilter.bind(this);
    this.getPostsByPaginatedLink = this.getPostsByPaginatedLink.bind(this);
  }

  // Filter posts based on status or tag(s).
  filterPosts(filters) {
    this.setState({
      filters,
    });

    const formattedFilters = {
      campaign_id: this.props.campaign.id,
      status: filters.status,
    };

    // Grab all of the active tags to send to API request.
    if (filters.tags) {
      const activeTags = [];

      Object.keys(filters.tags).forEach(key => {
        if (filters.tags[key].active === true) {
          activeTags.push(key);
        }
      });

      if (activeTags.length > 0) {
        formattedFilters.tag = activeTags.toString();
      }
    }

    this.getPostsByFilter(formattedFilters);
  }

  // Make API call to paginated link to get next/previous batch of posts.
  getPostsByPaginatedLink(url, event) {
    event.preventDefault();

    this.setState({ loadingNewPosts: true });

    // Strip the url to get query parameters.
    const splitEndpoint = url.split('/');
    const path = splitEndpoint.slice(-1)[0];
    const queryString = path.split('?')[1];

    this.api.get('api/v3/posts', queryString).then(json => {
      this.setState({ loadingNewPosts: false });
      this.props.setNewPosts(json);
    });
  }

  // Make API call to GET api/v3/posts to get posts by filtered status and/or tag(s).
  getPostsByFilter(filters) {
    this.setState({ loadingNewPosts: true });

    this.api
      .get('api/v3/posts', {
        filter: filters,
        include: ['signup', 'siblings'],
      })
      .then(json => {
        this.setState({ loadingNewPosts: false });
        this.props.setNewPosts(json);
      });
  }

  render() {
    const posts = this.props.posts;
    const campaign = this.props.campaign;
    const signups = this.props.signups;
    const tags = this.state.filters.tags;
    const tagFilters = {
      values: tags,
      type: 'tags',
    };

    const statusFilters = {
      values: {
        accepted: 'Accepted',
        pending: 'Pending',
        rejected: 'Rejected',
      },
      type: 'status',
      default: 'accepted',
    };

    return (

              <StatusCounter
                postTotals={this.props.post_totals}
                campaign={campaign}
              />

              <UserExport campaign={campaign} />

        <FilterBar onSubmit={this.filterPosts}>
          <DropdownFilter options={statusFilters} header={'Post Status'} />
          <MultiValueFilter options={tagFilters} header={'Tags'} />
        </FilterBar>

Posts

        {this.props.loading || this.state.loadingNewPosts ? (

        ) : this.props.postIds.length !== 0 ? (
          map(this.props.postIds, (key, value) => {
            const post = find(posts, { id: key });

            return (
              <Post
                key={key}
                post={post}
                user={signups[post.signup_id].user.data}
                signup={signups[post.signup_id]}
                campaign={campaign}
                onUpdate={this.props.updatePost}
                onTag={this.props.updateTag}
                deletePost={this.props.deletePost}
                showHistory={this.props.showHistory}
                rotate={this.props.rotate}
                showSiblings
                showQuantity
                allowHistory
              />
            );
          })
        ) : (
          <Empty
            header="There are no results!"
            copy="Sorry, there are no posts that match your filters. Change or remove some tags and try again."
          />
        )}

        <ModalContainer>
          {this.props.displayHistoryModal ? (
            <HistoryModal
              id={this.props.historyModalId}
              onUpdate={this.props.updateQuantity}
              onClose={e => this.props.hideHistory(e)}
              campaign={campaign}
              signup={signups[posts[this.props.historyModalId].signup_id]}
              signupEvents={this.props.signupEvents}
              post={posts[this.props.historyModalId]}
            />
          ) : null}
        </ModalContainer>

        <PagingButtons
          onPaginate={this.getPostsByPaginatedLink}
          prev={this.props.prevPage}
          next={this.props.nextPage}
        />

    );
  }
}

CampaignSingle.propTypes = {
  campaign: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
  deletePost: PropTypes.func.isRequired,
  displayHistoryModal: PropTypes.bool,
  hideHistory: PropTypes.func.isRequired,
  historyModalId: PropTypes.number,
  loading: PropTypes.bool.isRequired,
  postIds: PropTypes.arrayOf(PropTypes.number),
  posts: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  rotate: PropTypes.func.isRequired,
  setNewPosts: PropTypes.func.isRequired,
  showHistory: PropTypes.func.isRequired,
  signups: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  signupEvents: PropTypes.array, // eslint-disable-line react/forbid-prop-types
  updatePost: PropTypes.func.isRequired,
  updateQuantity: PropTypes.func.isRequired,
  updateTag: PropTypes.func.isRequired,
};

CampaignSingle.defaultProps = {
  displayHistoryModal: null,
  historyModalId: null,
  postIds: null,
  posts: null,
  signups: null,
  signupEvents: null,
};

export default CampaignSingle;
<commit_msg>adds new(ish) tags to filter bar<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { map, find } from 'lodash';
import { RestApiClient } from '@dosomething/gateway';

import Post from '../Post';
import Empty from '../Empty';
import FilterBar from '../FilterBar';
import HistoryModal from '../HistoryModal';
import PagingButtons from '../PagingButtons';
import StatusCounter from '../StatusCounter';
import DropdownFilter from '../DropdownFilter';
import ModalContainer from '../ModalContainer';
import MultiValueFilter from '../MultiValueFilter';
import UserExport from '../UserExport';

class CampaignSingle extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      loadingNewPosts: false,
      filters: {
        status: 'accepted',
        tags: {
          'good-submission': {
            label: 'Good Submission',
            active: false,
          },
          'good-quote': {
            label: 'Good Quote',
            active: false,
          },
          'hide-in-gallery': {
            label: 'Hide In Gallery 👻',
            active: false,
          },
          'group-photo': {
            label: 'Group Photo',
            active: false,
          },
          'good-for-sponsor': {
            label: 'Good For Sponsor',
            active: false,
          },
          'good-for-storytelling': {
            label: 'Good For Storytelling',
            active: false,
          },
          'good-for-brand': {
            label: 'Good For Brand',
            active: false,
          },
          bulk: {
            label: 'Bulk',
            active: false,
          },
          irrelevant: {
            label: 'Irrelevant',
            active: false,
          },
          inappropriate: {
            label: 'Inappropriate',
            active: false,
          },
          'unrealistic-quantity': {
            label: 'Unrealistic Quantity',
            active: false,
          },
          test: {
            label: 'Test',
            active: false,
          },
          'incomplete-action': {
            label: 'Incomplete Action',
            active: false,
          },
        },
      },
    };

    this.api = new RestApiClient(window.location.origin, {
      headers: { Authorization: `Bearer ${window.AUTH}` },
    });
    this.filterPosts = this.filterPosts.bind(this);
    this.getPostsByFilter = this.getPostsByFilter.bind(this);
    this.getPostsByPaginatedLink = this.getPostsByPaginatedLink.bind(this);
  }

  // Filter posts based on status or tag(s).
  filterPosts(filters) {
    this.setState({
      filters,
    });

    const formattedFilters = {
      campaign_id: this.props.campaign.id,
      status: filters.status,
    };

    // Grab all of the active tags to send to API request.
    if (filters.tags) {
      const activeTags = [];

      Object.keys(filters.tags).forEach(key => {
        if (filters.tags[key].active === true) {
          activeTags.push(key);
        }
      });

      if (activeTags.length > 0) {
        formattedFilters.tag = activeTags.toString();
      }
    }

    this.getPostsByFilter(formattedFilters);
  }

  // Make API call to paginated link to get next/previous batch of posts.
  getPostsByPaginatedLink(url, event) {
    event.preventDefault();

    this.setState({ loadingNewPosts: true });

    // Strip the url to get query parameters.
    const splitEndpoint = url.split('/');
    const path = splitEndpoint.slice(-1)[0];
    const queryString = path.split('?')[1];

    this.api.get('api/v3/posts', queryString).then(json => {
      this.setState({ loadingNewPosts: false });
      this.props.setNewPosts(json);
    });
  }

  // Make API call to GET api/v3/posts to get posts by filtered status and/or tag(s).
  getPostsByFilter(filters) {
    this.setState({ loadingNewPosts: true });

    this.api
      .get('api/v3/posts', {
        filter: filters,
        include: ['signup', 'siblings'],
      })
      .then(json => {
        this.setState({ loadingNewPosts: false });
        this.props.setNewPosts(json);
      });
  }

  render() {
    const posts = this.props.posts;
    const campaign = this.props.campaign;
    const signups = this.props.signups;
    const tags = this.state.filters.tags;
    const tagFilters = {
      values: tags,
      type: 'tags',
    };

    const statusFilters = {
      values: {
        accepted: 'Accepted',
        pending: 'Pending',
        rejected: 'Rejected',
      },
      type: 'status',
      default: 'accepted',
    };

    return (

              <StatusCounter
                postTotals={this.props.post_totals}
                campaign={campaign}
              />

              <UserExport campaign={campaign} />

        <FilterBar onSubmit={this.filterPosts}>
          <DropdownFilter options={statusFilters} header={'Post Status'} />
          <MultiValueFilter options={tagFilters} header={'Tags'} />
        </FilterBar>

Posts

        {this.props.loading || this.state.loadingNewPosts ? (

        ) : this.props.postIds.length !== 0 ? (
          map(this.props.postIds, (key, value) => {
            const post = find(posts, { id: key });

            return (
              <Post
                key={key}
                post={post}
                user={signups[post.signup_id].user.data}
                signup={signups[post.signup_id]}
                campaign={campaign}
                onUpdate={this.props.updatePost}
                onTag={this.props.updateTag}
                deletePost={this.props.deletePost}
                showHistory={this.props.showHistory}
                rotate={this.props.rotate}
                showSiblings
                showQuantity
                allowHistory
              />
            );
          })
        ) : (
          <Empty
            header="There are no results!"
            copy="Sorry, there are no posts that match your filters. Change or remove some tags and try again."
          />
        )}

        <ModalContainer>
          {this.props.displayHistoryModal ? (
            <HistoryModal
              id={this.props.historyModalId}
              onUpdate={this.props.updateQuantity}
              onClose={e => this.props.hideHistory(e)}
              campaign={campaign}
              signup={signups[posts[this.props.historyModalId].signup_id]}
              signupEvents={this.props.signupEvents}
              post={posts[this.props.historyModalId]}
            />
          ) : null}
        </ModalContainer>

        <PagingButtons
          onPaginate={this.getPostsByPaginatedLink}
          prev={this.props.prevPage}
          next={this.props.nextPage}
        />

    );
  }
}

CampaignSingle.propTypes = {
  campaign: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
  deletePost: PropTypes.func.isRequired,
  displayHistoryModal: PropTypes.bool,
  hideHistory: PropTypes.func.isRequired,
  historyModalId: PropTypes.number,
  loading: PropTypes.bool.isRequired,
  postIds: PropTypes.arrayOf(PropTypes.number),
  posts: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  rotate: PropTypes.func.isRequired,
  setNewPosts: PropTypes.func.isRequired,
  showHistory: PropTypes.func.isRequired,
  signups: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  signupEvents: PropTypes.array, // eslint-disable-line react/forbid-prop-types
  updatePost: PropTypes.func.isRequired,
  updateQuantity: PropTypes.func.isRequired,
  updateTag: PropTypes.func.isRequired,
};

CampaignSingle.defaultProps = {
  displayHistoryModal: null,
  historyModalId: null,
  postIds: null,
  posts: null,
  signups: null,
  signupEvents: null,
};

export default CampaignSingle;
<|endoftext|>