Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
GuiConnector.js 17.32 KiB
"use strict";

var Promise = require("bluebird");

var logger = require('./logger');

var Functions = require('./Functions');
var SecurityError = require('./SecurityError');
var ValidationError = require('./ValidationError');

/**
 * This static global object contains set of functions that returns/set data in
 * the Gui (html).
 */
function GuiConnector() {
  // X coordinate of the mouse in a browser.
  //@type {number}
  this.xPos = 0;
  // coordinate of the mouse in a browser.
  //@type {number}
  this.yPos = 0;

  this.getParams = [];
}

/**
 *
 * @param {GuiConnector} object
 * @returns {GuiConnector}
 */
function returnThisOrSingleton(object) {
  if (object === undefined || object === null) {
    return GuiConnector.singleton;
  } else {
    return object;
  }
}

/**
 * List of GET params passed via url.
 */

GuiConnector.prototype.init = function () {
  var self = returnThisOrSingleton(this);

  if (!String.prototype.endsWith) {
    String.prototype.endsWith = function (pattern) {
      var d = this.length - pattern.length;
      return d >= 0 && this.lastIndexOf(pattern) === d;
    };
  }
  // noinspection PointlessBooleanExpressionJS,JSUnresolvedVariable
  var isIE = /* @cc_on!@ */false || !!document.documentMode;

  if (isIE) {
    alert("This web page works well with Chrome, Firefox and Safari.");
  }
  // bootstrap tab initialization
  $("ul.nav-tabs a").click(function (e) {
    e.preventDefault();
    $(this).tab('show');
  });

  self.getParams = [];

  // find GuiConnector.getParams
  window.location.search.replace(/\??(?:([^=]+)=([^&]*)&?)/g, function () {
    function decode(s) {
      return decodeURIComponent(s.split("+").join(" "));
    }

    self.getParams[decode(arguments[1])] = decode(arguments[2]);
  });

  self._touchStartEvent = function (e) {
    if (e.originalEvent !== undefined) {
      self.updateMouseCoordinates(e.originalEvent.touches[0].pageX, e.originalEvent.touches[0].pageY);
    }
  };
  self._touchMoveEvent = function (e) {
    if (e.originalEvent !== undefined) {
      self.updateMouseCoordinates(e.originalEvent.touches[0].pageX, e.originalEvent.touches[0].pageY);
    }
  };

  // force browser to update mouse coordinates whenever mouse move
  jQuery(document).ready(function () {
    $(document).mousemove(function (e) {
      self.updateMouseCoordinates(e.pageX, e.pageY);
    });
    $(document).on('touchstart', self._touchStartEvent);
    $(document).on('touchmove', self._touchMoveEvent);
  });

  if (self._windowResizeEvents === undefined) {
    self._windowResizeEvents = [];

    if (window.onresize !== null && window.onresize !== undefined) {
      self.addWindowResizeEvent(window.onresize);
    }

    window.onresize = function () {
      for (var i = 0; i < self._windowResizeEvents.length; i++) {
        self._windowResizeEvents[i]();
      }
    };
  }
  newUrl = "";

  //sorting of datatable column by input value https://stackoverflow.com/a/29221907/1127920
  $.fn.dataTable.ext.order['dom-input'] = function (settings, col) {
    return this.api().column(col, {order: 'index'}).nodes().map(function (td, i) {
      return $('input', td).val();
    });
  }
};


var newUrl = "";

setInterval(function () {
  if (global.window !== undefined && newUrl !== "") {
    if (!global.window.location.href.endsWith(newUrl)) {
      global.window.history.replaceState(null, null, newUrl);
    }
  }
}, 250);


/**
 *
 * @param {string} key
 * @param {string} value
 */
GuiConnector.prototype.setUrlParam = function (key, value) {
  var self = this;
  if (value === null || value === "") {
    value = undefined;
  }
  if (self.getParams[key] !== value) {
    self.getParams[key] = value;
    var url = window.location.pathname + '?';
    for (var getParamKey in self.getParams) {
      if (self.getParams.hasOwnProperty(getParamKey)) {
        var getParamValue = self.getParams[getParamKey];
        if (getParamValue !== undefined) {
          url += getParamKey + "=" + getParamValue + "&";
        }
      }
    }
    newUrl = url;
  }
};

/**
 *
 * @param {function} handler
 */
GuiConnector.prototype.addWindowResizeEvent = function (handler) {
  this._windowResizeEvents.push(handler);
};

/**
 *
 * @param {function} handler
 */
GuiConnector.prototype.removeWindowResizeEvent = function (handler) {
  var events = this._windowResizeEvents;
  var index = events.indexOf(handler);
  if (index > -1) {
    events.splice(index, 1);
  } else {
    logger.warn("Cannot find listener", handler);
  }
};

/**
 * Returns name of the file with LCSB logo.
 *
 * @param bigLogo
 *          {@link Boolean} value determining if we want to have big logo or
 *          small one
 * @returns {string}
 */
GuiConnector.prototype.getLcsbLogoImg = function (bigLogo) {
  if (bigLogo) {
    return 'lcsb_logo_mid.png';
  } else {
    return 'lcsb_logo.png';
  }
};

/**
 * Returns name of the file with image that should be presented when we are
 * waiting for data to be loaded.
 * @returns {string}
 */
GuiConnector.prototype.getLoadingImg = function () {
  return "icons/ajax-loader.gif";
};

/**
 *
 * @returns {string}
 */
GuiConnector.prototype.getEmptyTileUrl = function () {
  return "resources/images/empty_tile.png";
};

/**
 * Returns home directory for images in the application.
 * @returns {string}
 */
GuiConnector.prototype.getImgPrefix = function () {
  return "resources/images/";
};

/**
 * Updates coordinates of the mouse in the browser.
 *
 * @param {number} x
 * @param {number} y
 */
GuiConnector.prototype.updateMouseCoordinates = function (x, y) {
  var self = returnThisOrSingleton(this);
  self.xPos = x;
  self.yPos = y;
};

/**
 *
 * @param {string} [messageText]
 */
GuiConnector.prototype.showProcessing = function (messageText) {
  var self = returnThisOrSingleton(this);
  if (self._processingDialog === undefined) {
    self._processingDialog = document.createElement("div");
    self._processingDialogContent = document.createElement("div");
    self._processingDialog.appendChild(self._processingDialogContent);
    document.body.appendChild(self._processingDialog);
    $(self._processingDialog).dialog({
      modal: true,
      title: "PROCESSING",
      width: "150px",
      closeOnEscape: false,
      dialogClass: 'minerva-no-close'
    });
  }
  if (messageText === undefined) {
    messageText = "PROCESSING";
  }
  var messageImg = Functions.createElement({
    type: "img",
    src: 'resources/images/icons/ajax-loader.gif'
  });
  self._processingDialogContent.innerHTML = "";
  self._processingDialogContent.style.textAlign = "center";
  self._processingDialogContent.appendChild(messageImg);

  $(self._processingDialog).dialog("option", "title", messageText);

  $(self._processingDialog).dialog("open");
};

/**
 *
 */
GuiConnector.prototype.hideProcessing = function () {
  var self = returnThisOrSingleton(this);
  $(self._processingDialog).dialog("close");
};

GuiConnector.prototype.showErrorDialog = function (title, content) {
  var dialog = document.createElement('div');
  dialog.title = title;
  var dialogBody = document.createElement('p');
  dialogBody.innerHTML = content;
  dialog.appendChild(dialogBody);
  $(dialog).dialog({
    modal: true,
    dialogClass: 'minerva-error-dialog',
    classes: {
      "ui-dialog": "ui-state-error"
    },
    close: function () {
      $(this).dialog('destroy').remove();
    }
  }).siblings('.ui-dialog-titlebar').css("background", "red");
};

GuiConnector.prototype.showSuccessDialog = function (title, content) {
  var dialog = document.createElement('div');
  dialog.title = title;
  var dialogBody = document.createElement('p');
  dialogBody.innerHTML = content;
  dialog.appendChild(dialogBody);
  $(dialog).dialog({
    dialogClass: 'minerva-success-dialog',
    modal: true,
    close: function () {
      $(this).dialog('destroy').remove();
    }
  }).siblings('.ui-dialog-titlebar').css("background", "green");
};

/**
 * Gather information that are presented to the user before submission to MinervaNet.
 *
 * @param {string|Error} error The error that triggered the report sequence.
 * @return {Promise}
 */
GuiConnector.prototype.gatherReportData = function () {
  return ServerConnector.getLoggedUser().then(function (user) {
    return {
      url: {
        value: window.location.href,
        tooltip: 'The error location. This information can help to narrow down the error source.'
      },
      login: {
        value: user.getLogin(),
        tooltip: 'Your account name. This information is useful in case the issue is specific to a certain account.'
      },
      email: {
        value: user.getEmail(),
        tooltip: 'Your contact email. If provided we might contact you for additional information.'
      },
      browser: {
        value: navigator.userAgent,
        tooltip: 'Your browser user agent. Many issues are specific to certain browsers. This information is important to identify those.'
      },
      timestamp: {
        value: Math.floor(+new Date() / 1000),
        tooltip: 'The error time. This information is useful to link the issue to a specific event on the server.'
      } // TODO: Submission time rather than server time for now
    };
  });
};

/**
 *
 * @param {string|Error} error
 * @param {boolean} [redirectIfSecurityError]
 */
GuiConnector.prototype.alert = function (error, redirectIfSecurityError) {
  error = error || '';
  if (redirectIfSecurityError && error instanceof SecurityError && ServerConnector.getSessionData().getLogin() === "anonymous") {
    window.location.href = ServerConnector.getServerBaseUrl() + "login.xhtml?from=" + encodeURI(window.location.href);
  } else {
    var self = returnThisOrSingleton(this);
    logger.error(error);
    var errorData = self.getErrorMessageForError(error);
    if (!errorData.showReport) {
      self.showErrorDialog("An error occurred!", errorData.message);
    } else {
      self._errorDialog = document.createElement('div');
      self._errorDialog.innerHTML = '<span class="ui-icon ui-icon-info" style="float: right;" title="The error message. This might not be human readable. If this issue persists you should should contact your administrator."></span>' +
        '<span>' + errorData.message + '</span>';
      self.gatherReportData().then(function (data) {
        self._errorDialog.innerHTML += '<p class="report-dialog-warning">If you agree to submit the following information to the Minerva maintainers please uncheck all boxes that might contain sensitive data.</p>';
        self._errorDialogData = document.createElement('div');
        self._errorDialog.appendChild(self._errorDialogData);
        self._errorDialogData.innerHTML += '<textarea id="report-comment" maxlength="255" placeholder="Add comment..."></textarea>';
        Object.keys(data).forEach(function (key) {
          self._errorDialogData.innerHTML += '<label>' +
            (key === 'timestamp' ? new Date(data[key].value * 1000) : data[key].value) +
            '<input class="report-check" type="checkbox" data-key="' + key + '" data-value="' + data[key].value + '"/>' +
            '<span class="ui-icon ui-icon-info" title="' + data[key].tooltip + '"></span>' +
            '</label>';
        });
        self._errorDialogData.innerHTML += '<div id="report-stacktrace">' +
          '<h3>Stacktrace' +
          '<span class="ui-icon ui-icon-info" title="' + 'The error stacktrace. The sequence of events that triggered this particular error.' + '"></span>' +
          '</h3>' +
          '<div><p>' + errorData.stacktrace + '</p></div>' +
          '</div>';
        $('#report-stacktrace')
          .accordion({active: false, collapsible: true});
        $('.report-check')
          .checkboxradio()
          .prop('checked', true)
          .button('refresh');
        $(self._errorDialogData)
          .controlgroup({direction: 'vertical'});
        $(self._errorDialog)
          .tooltip({
            classes: {
              "ui-tooltip": "report-tooltip ui-corner-all ui-widget-shadow"
            },
            track: true,
            position: {
              my: 'right',
              at: 'left'
            }
          });
      });
      document.body.appendChild(self._errorDialog);
      $(self._errorDialog).dialog({
        classes: {
          'ui-dialog': 'report-dialog ui-corner-all',
          'ui-dialog-titlebar': 'ui-corner-all'
        },
        title: 'An error occurred!',
        resizable: true,
        height: 'auto',
        width: '500px',
        modal: true,
        close: function () {
          $(this).dialog('destroy').remove();
        },
        buttons: {
          'Submit': function () {
            var report = {
              stacktrace: errorData.stacktrace,
              comment: $('#report-comment').val()
            };
            $('.report-check').each(function () {
              var check = $(this);
              if (check.is(':checked')) {
                report[check.attr('data-key')] = check.attr('data-value');
              }
            });
            ServerConnector.submitErrorToMinervaNet(report, function (error, response) {
              if (error || response.statusCode !== 200) {
                self.showErrorDialog('Report could not be submitted!',
                  'Please contact your system administrator if this issue persists.');
              } else {
                self.showSuccessDialog('Report has been submitted!',
                  'Thank you very much for helping us to improve Minerva.');
              }
            });
            $(this).dialog('destroy').remove();
          },
          'Cancel': function () {
            $(this).dialog('destroy').remove();
          }
        }
      }).siblings('.ui-dialog-titlebar').css("background", "red");
    }
  }
};

/**
 *
 * @param {Error|string} error
 * @returns {Object}
 */
GuiConnector.prototype.getErrorMessageForError = function (error) {
  var expectedError = typeof error === 'string' || error instanceof SecurityError || error instanceof ValidationError;
  var errorData = {
    showReport: !expectedError,
    message: typeof error === 'string' ? error : error.message,
    stacktrace: error.stack
  };

  if (error instanceof SecurityError) {
    if (ServerConnector.getSessionData().getLogin() === "anonymous") {
      errorData.message = "<p>Please <a href=\"login.xhtml?from=" + encodeURI(window.location.href) + "\">login</a> to access this resource</p>";
    } else {
      errorData.message += "<p>Please <a href=\"login.xhtml?from=" + encodeURI(window.location.href) + "\">login</a> " + "as a different user or ask your administrator to change the permissions to access this resource.</p>";
    }
  }
  return errorData;
};

/**
 *
 * @param {string} message
 */
GuiConnector.prototype.info = function (message) {
  var self = returnThisOrSingleton(this);
  if (self._infoDialog === undefined) {
    self._infoDialog = document.createElement("div");
    self._infoDialogContent = document.createElement("div");
    self._infoDialog.appendChild(self._infoDialogContent);
    document.body.appendChild(self._infoDialog);
    $(self._infoDialog).dialog({
      dialogClass: 'minerva-info-dialog',
      classes: {
        "ui-dialog": "ui-state-info"
      },
      modal: true,
      title: "INFO"
    });
  }
  self._infoDialogContent.innerHTML = message;
  $(self._infoDialog).dialog("open");

};

/**
 *
 * @param {{message:string, [title]:string}} params
 */
GuiConnector.prototype.showConfirmationDialog = function (params) {
  var message = params.message;
  var title = params.title;
  if (title === undefined) {
    title = "Confirm";
  }
  return new Promise(function (resolve) {
    $('<div></div>').appendTo('body')
      .html('<div><h6>' + message + '</h6></div>')
      .dialog({
        dialogClass: 'minerva-confirmation-dialog',
        modal: true, title: title, zIndex: 10000, autoOpen: true,
        width: 'auto', resizable: false,
        buttons: {
          Yes: function () {
            $(this).dialog("close");
            resolve(true);
          },
          No: function () {
            $(this).dialog("close");
            resolve(false);
          }
        },
        close: function (event, ui) {
          $(this).remove();
        }
      });
  });
};

/**
 *
 */
GuiConnector.prototype.destroy = function () {
  var self = returnThisOrSingleton(this);

  if (self._infoDialog !== undefined) {
    $(self._infoDialog).dialog("destroy").remove();
  }
  if (self._warnDialog !== undefined) {
    $(self._warnDialog).dialog("destroy").remove();
    self._warnDialog = undefined;
  }
  if (self._processingDialog !== undefined) {
    $(self._processingDialog).dialog("destroy").remove();
  }

  if (self._errorDialog !== undefined) {
    $(self._errorDialog).dialog("destroy").remove();
  }

  self._windowResizeEvents = undefined;
  $(document).off('touchstart', self._touchStartEvent);
  $(document).off('touchmove', self._touchMoveEvent);

};

/**
 *
 * @param {string} message
 */
GuiConnector.prototype.warn = function (message) {
  var self = returnThisOrSingleton(this);
  logger.warn(message);
  if (self._warnDialog === undefined) {
    self._warnDialog = document.createElement("div");
    self._warnDialogContent = document.createElement("div");
    self._warnDialog.appendChild(self._warnDialogContent);
    document.body.appendChild(self._warnDialog);
    $(self._warnDialog).dialog({
      dialogClass: 'minerva-warn-dialog',
      classes: {
        "ui-dialog": "ui-state-highlight"
      },
      modal: true,
      title: "WARNING"
    });
  }
  self._warnDialogContent.innerHTML = message;
  $(self._warnDialog).dialog("open");
};

GuiConnector.singleton = new GuiConnector();

module.exports = GuiConnector.singleton;