jiskattema/spot

View on GitHub
src/app.js

Summary

Maintainability
F
1 wk
Test Coverage
var Spot = require('spot-framework');
var app = require('ampersand-app');
var Router = require('./router');
var MainView = require('./pages/main');
var DatasetsView = require('./pages/datasets');
var domReady = require('domready');
var widgetFactory = require('./widgets/widget-factory');
var viewFactory = require('./widgets/view-factory');
var Collection = require('ampersand-collection');

var SessionModel = require('./pages/datasets/session-model');
var dialogPolyfill = require('dialog-polyfill');

var Help = require('intro.js');
var templates = require('./templates');
var csv = require('csv');
var $ = require('jquery');

require('babel-polyfill');
require('mdl');

var sessionCollection = Collection.extend({
  mainIndex: 'id',
  indexes: ['name'],
  model: SessionModel
});

// attach our app to `window` so we can
// easily access it from the console.
window.app = app;

// Extends our main app singleton
app.extend({
  /**
   * [fullscreenMode description]
   * @type {Boolean}
   */
  fullscreenMode: false,
  /**
   * [demoSession description]
   * @type {Boolean}
   */
  demoSession: false,
  /**
   * [mobileBrowser description]
   * @type {Boolean}
   */
  mobileBrowser: false,
  /**
   * [me description]
   * @type {Spot}
   */
  me: new Spot(),
  /**
   * [widgetFactory description]
   * @type {any}
   */
  widgetFactory: widgetFactory,
  /**
   * [viewFactory description]
   * @type {any}
   */
  viewFactory: viewFactory,
  /**
   * [router description]
   * @type {Router}
   */
  router: new Router(),
  /**
   * [CSVSeparator description]
   * @type {String}
   */
  CSVSeparator: ',',
  /**
   * [CSVHeaders description]
   * @type {Boolean}
   */
  CSVHeaders: true,
  /**
   * [CSVQuote description]
   * @type {String}
   */
  CSVQuote: '"',
  /**
   * [CSVComment description]
   * @type {String}
   */
  CSVComment: '#',

  helper: {enabled: false, instance: new Help()},

  /**
   * [sessions description]
   * @type {any}
   */
  sessions: new sessionCollection(),
  /**
   * This is where it all starts
   */
  init: function () {
    // Create and attach our main view
    this.mainView = new MainView({
      model: this.me,
      el: document.body
    });

    // this kicks off our backbutton tracking (browser history)
    // and will cause the first matching handler in the router
    // to fire.
    this.router.history.start({
      root: '/',
      pushState: true,
      hashChange: true
    });
  },
  /**
   * This is a helper for navigating around the app.
     this gets called by a global click handler that handles
     all the <a> tags in the app.
     it expects a url pathname for example: "/costello/settings"
   * @param  {any} page [description]
   */
  navigate: function (page) {

    // clean all help items before navigating to new page
    app.stopHelp();

    var url = (page.charAt(0) === '/') ? page.slice(1) : page;
    this.router.history.navigate(url, {
      trigger: true
    });
  },
  /**
   * [description]
   * @param  {any} percentage [description]
   */
  progress: function (percentage) {
    var progressBar = document.getElementById('progress-bar');
    progressBar.MaterialProgress.setProgress(percentage);

    progressBar.style.display = 'inherit';
  },
  /**
   * [description]
   * @param  {boolean} status [description]
   */
  busy: function (callBack) {
    var that = this;

    var dialog = document.getElementById('main-dialog');
    dialogPolyfill.registerDialog(dialog);

    console.log(dialog);
    dialog.open = !dialog.open;
    console.log(dialog);
  },
  /**
   * [description]
   * @param  {any} options [description]
   */
  message: function (options) {
    var snackbarContainer = document.getElementById('snack-bar');
    var snackData = { message: options.text };

    // BUGFIX: during app initialization, the snackbar is not always ready yet
    if (!snackbarContainer.MaterialSnackbar) {
      return;
    }

    var progressBar = document.getElementById('progress-bar');
    progressBar.style.display = 'none';

    if (options.type === 'error') {
      console.warn(options.text, options.error);
      snackData.timeout = 10000; // show error for 10 seconds
    } else {
      console.log(options.text);
      snackData.timeout = 2750;
    }
    snackbarContainer.MaterialSnackbar.showSnackbar(snackData);
  },

  /**
   * [description]
   */
  startHelp: function () {

    console.log(app.currentPage.helpTemplate);
    console.log(app.currentPage.helpHints);
    console.log(app.currentPage.helpSteps);
    if (
      (!app.currentPage.helpTemplate || app.currentPage.helpTemplate === '') &&
      (!app.currentPage.helpHints || app.currentPage.helpHints() === []) &&
      (!app.currentPage.helpSteps || app.currentPage.helpTemplate === [])
    ) {
      console.log('No Help item was found for this page! Exiting.')
      return;
    }

    console.log(app.helper.enabled);

    if (app.helper.enabled) {
      console.log('Closing existing help!');
      app.stopHelp();
      return;
    }

    app.helper.enabled = true;

    console.log("app.helper: ", app.helper);

    if (app.currentPage.helpTemplate && app.currentPage.helpTemplate !== '') {
      console.log("Setting intros...");
      app.helper.instance.setOptions({
        steps: [
          {
            intro: window[app.currentPage.helpTemplate]()
          }
        ]
      });
    }

    app.helper.instance.setOptions({
      hints: app.currentPage.helpHints(),
      steps: app.currentPage.helpSteps()
    });

    app.helper.instance.onhintsadded(function() {
        console.log('all hints added');
    });
    app.helper.instance.onhintclick(function(hintElement, item, stepId) {
        console.log('hint clicked', hintElement, item, stepId);
    });
    app.helper.instance.onhintclose(function (stepId) {
        console.log('hint closed', stepId);
    });

    app.helper.instance.addHints();
    app.helper.instance.showHints();
    // app.helper.start();
  },
  stopHelp: function () {
    if (app.helper.enabled) {
      console.log('Closing existing help!');
      // app.helper.instance.helper.exit();
      app.helper.instance.hideHints();
      app.helper.enabled = false;
      return;
    }
  },
  /**
   * [description]
   */
  startWelcome: function () {
    var welcome = Help();
    welcome.setOption('tooltipClass', 'welcome-dialog');
    welcome.setOptions({
      'showStepNumbers': false,
      'showBullets': false,
      'showProgress': false,
      'skipLabel': 'Exit',
      'doneLabel': 'Start demo',
      'tooltipPosition': 'auto',
      steps: [
        {
          intro: templates.help.welcome()
        }
      ]
    });

    welcome.onchange(function (targetElement) {
      if (this._currentStep === this._introItems.length - 1) {
        $('.introjs-skipbutton').css('color', 'green');
      }
    });

    welcome.oncomplete(function () {
      window.localStorage.setItem('spotWelcome', 'done');
      app.message({
        text: 'Starting the demo session.',
        type: 'ok'
      });
      app.importRemoteSession('https://raw.githubusercontent.com/NLeSC/spot/master/dist/demo.json');
    });

    // add a flag when we exit
    welcome.onexit(function () {
      window.localStorage.setItem('spotWelcome', 'done');
    });

    var spotWelcome = window.localStorage.getItem('spotWelcome') === 'done';
    if (spotWelcome) {
      // console.log('No need to show welcome dialog again.');
    } else {
      console.log('Starting the welcome dialog.');
      welcome.start();
    }
  },
  /**
   * [description]
   * @return {boolean} [description]
   */
  detectMobile: function () {
    var check = false;
    if (navigator.userAgent.match(/Android/i) ||
    navigator.userAgent.match(/webOS/i) ||
    navigator.userAgent.match(/iPhone/i) ||
    navigator.userAgent.match(/iPad/i) ||
    navigator.userAgent.match(/iPod/i) ||
    navigator.userAgent.match(/BlackBerry/i) ||
    navigator.userAgent.match(/Windows Phone/i)
   ) {
      check = true;
    } else {
      check = false;
    }
    app.mobileBrowser = check;
    return check;
  },

  /**
   * [description]
   * @return {} [description]
   */
  addDatasetToLocalStorage: function(dataset) {
    console.log('Adding a dataset to the local storage');
    console.log(dataset);
    var allDatasets = this.getDatasetsFromLocalStorage();
    // allDatasets.forEach(function(dset, index) {
    //   console.log("[" + index + "]: " + dset.id + '  ', dset.name);
    // });
    allDatasets.push(dataset);
    localStorage.setItem('datasets', JSON.stringify(allDatasets));
  },
    /**
   * [description]
   * @return {} [description]
   */
  removeDatasetFromLocalStorage: function(dataset) {
    console.log('Removing a dataset from the local storage');
    console.log(dataset);
    var allDatasets = this.getDatasetsFromLocalStorage();
    allDatasets.forEach(function(dset, index) {
      console.log("[" + index + "]: " + dset.id + '  ', dset.name);
      if ( dataset.id === dset.id )
        allDatasets.splice(index, 1);
    });
    // var index = allDatasets.indexOf(dataset);
    // if (index > -1) {
    //   allDatasets.splice(index, 1);
    // }
    localStorage.setItem('datasets', allDatasets);
  },
    /**
   * [description]
   * @return {} [description]
   */
  getDatasetsFromLocalStorage: function() {
    console.log('Getting a list of datasets from the local storage');
    var allDatasets = JSON.parse(localStorage.getItem('datasets') || "[]");
    return allDatasets;
  },
  /**
   * [description]
   * @return {} [description]
   */
  addSessionToLocalStorage: function(session) {
    console.log('Adding a session to the local storage');
    console.log(session);
    var allSessions = this.getSessionsFromLocalStorage();
    // allDatasets.forEach(function(dset, index) {
    //   console.log("[" + index + "]: " + dset.id + '  ', dset.name);
    // });
    allSessions.push(session);
    localStorage.setItem('sessions', JSON.stringify(allSessions));
  },
    /**
   * [description]
   * @return {} [description]
   */
  removeSessionFromLocalStorage: function(input_session) {
    console.log('Removing a session from the local storage');
    console.log(input_session);
    var allSessions = this.getSessionsFromLocalStorage();
    allSessions.forEach(function(sess, index) {
      console.log("[" + index + "]: " + sess.id + '  ', sess.name);
      if ( input_session.id === sess.id )
        allSessions.splice(index, 1);
    });

    localStorage.setItem('sessions', allSessions);
  },
    /**
   * [description]
   * @return {} [description]
   */
  getSessionsFromLocalStorage: function() {
    console.log('Getting a list of sessions from the local storage');
    var allSessions = JSON.parse(localStorage.getItem('sessions') || "[]");
    return allSessions;
  },
  getCurrentSession: function () {
    var json = app.me.toJSON();
    if (app.me.sessionType === 'client') {
      // for client datasets, also save the data in the session file
      app.me.datasets.forEach(function (dataset, i) {
        json.datasets[i].data = dataset.data;
      });
    }
    // json.saveDate = Date().toLocaleString();
    var currentSession = json;
    return currentSession;
  },
  saveCurrentSession: function () {
    var currentSession = this.getCurrentSession();
    this.addSessionToLocalStorage(currentSession);
  },
  importRemoteSession: function (sessionUrl) {
    // console.log('app.js: Getting the remote session.');
    var that = this;

    app.busy({enable: true});

    var urlParts = sessionUrl.replace('http://','').replace('https://','').split(/[/?#]/);
    var domain = urlParts[0];

    if ( (domain === "sandbox.zenodo.org") || (domain === "zenodo.org") ) {
      // get files using a proxy to fix CORS issues
      sessionUrl = 'http://localhost:8000/' + sessionUrl;
    }

    that.zenodoRequest({
      base_url: sessionUrl,
      url_addition:"",
      requestType:"download",
      zenodoId: '',
      fileHash: ''
    }).then(function(download_data) {
      // console.log(download_data);
      app.busy({enable: false});
      app.message({
        text: 'Session was imported succesfully',
        type: 'ok'
      });
      app.loadSessionBlob(download_data);
    }).catch(function(error_download){
      app.busy({enable: false});
      app.message({
        text: 'Could not import the session',
        type: 'error',
        error: ev
      });
      console.error(error_download);
    });

  },
  /**
   * [description]
   * @param  {any} data [description]
   */
  loadSessionBlob: function (data) {
    console.log('Loading the session.');
    app.me = new Spot(data);

    if (data.sessionType === 'server') {
      app.me.connectToServer(data.address);
    } else if (data.sessionType === 'client') {
      // add data from the session file to the dataset
      data.datasets.forEach(function (d, i) {
        app.me.datasets.models[i].crossfilter.add(d.data);
        app.me.datasets.models[i].isActive = false; // we'll turn it on later
      });
      // merge all the data into the app.me.dataview
      // by toggling the active datasets back on
      data.datasets.forEach(function (d, i) {
        if (d.isActive) {
          app.me.toggleDataset(app.me.datasets.models[i]);
        }
      });
    }
    // and automatically go to the analyze page
    app.navigate('/analyze');    
    app.navigate('/datasets');
    app.navigate('/analyze');
  },
  importJSON: function () {
    // var fileLoader = this.queryByHook('json-upload-input');
    var fileLoader = document.getElementById('jsonuploadBtn');
    var uploadedFile = fileLoader.files[0];
    var reader = new window.FileReader();
    var dataURL = fileLoader.files[0].name;

    // TODO: enforce spot.driver === 'client'

    var dataset = app.me.datasets.add({
      name: dataURL,
      URL: dataURL,
      description: 'uploaded JSON file'
    });

    reader.onload = function (ev) {
      app.message({
        text: 'Processing',
        type: 'ok'
      });
      try {
        dataset.data = JSON.parse(ev.target.result);

        // automatically analyze dataset
        dataset.scan();
        dataset.facets.forEach(function (facet, i) {
          if (i < 20) {
            facet.isActive = true;

            if (facet.isCategorial) {
              facet.setCategories();
            } else if (facet.isContinuous || facet.isDatetime || facet.isDuration) {
              facet.setMinMax();
            }
          }
        });
        app.message({
          text: dataURL + ' was uploaded succesfully. Configured ' + dataset.facets.length + ' facets',
          type: 'ok'
        });
        window.componentHandler.upgradeDom();

        // Automatically activate dataset if it is the only one
        if (app.me.datasets.length === 1) {
          $('.mdl-switch').click(); // only way to get the switch in the 'on' position
        }
      } catch (ev) {
        app.me.datasets.remove(dataset);
        app.message({
          text: 'Error parsing JSON file: ' + ev,
          type: 'error',
          error: ev
        });
      }
    };

    reader.onerror = function (ev) {
      var error = ev.srcElement.error;

      app.message({
        text: 'File loading problem: ' + error,
        type: 'error',
        error: ev
      });
    };

    reader.onprogress = function (ev) {
      if (ev.lengthComputable) {
        // ev.loaded and ev.total are ProgressEvent properties
        app.progress(parseInt(100.0 * ev.loaded / ev.total));
      }
    };

    reader.readAsText(uploadedFile);
  },
  importCSV: function () {
    // var fileLoader = this.queryByHook('csv-upload-input');
    var fileLoader = document.getElementById('csvuploadBtn');
    var uploadedFile = fileLoader.files[0];
    var reader = new window.FileReader();
    var dataURL = fileLoader.files[0].name;

    // TODO: enforce spot.driver === 'client'

    var dataset = app.me.datasets.add({
      name: dataURL,
      URL: dataURL,
      description: 'Imported CSV file'
    });

    reader.onload = function (ev) {
      app.message({
        text: 'Processing',
        type: 'ok'
      });
      var options = {
        columns: app.CSVHeaders, // treat first line as header with column names
        relax_column_count: false, // accept malformed lines
        delimiter: app.CSVSeparator, // field delimieter
        quote: app.CSVQuote, // String quoting character
        comment: app.CSVComment, // Treat all the characters after this one as a comment.
        trim: true // ignore white space around delimiter
      };

      csv.parse(ev.target.result, options, function (err, data) {
        if (err) {
          app.me.datasets.remove(dataset);
          app.message({
            text: 'Error parsing CSV file: ' + err.message,
            type: 'error',
            error: ev
          });
        } else {
          dataset.data = data;

          // automatically analyze dataset
          dataset.scan();
          dataset.facets.forEach(function (facet, i) {
            if (i < 20) {
              facet.isActive = true;

              if (facet.isCategorial) {
                facet.setCategories();
              } else if (facet.isContinuous || facet.isDatetime || facet.isDuration) {
                facet.setMinMax();
              }
            }
          });
          app.addDatasetToLocalStorage(dataset);
          app.message({
            text: dataURL + ' was uploaded succesfully. Configured ' + dataset.facets.length + ' facets',
            type: 'ok'
          });
          window.componentHandler.upgradeDom();

          // Automatically activate dataset if it is the only one
          if (app.me.datasets.length === 1) {
            $('.mdl-switch').click(); // only way to get the switch in the 'on' position
          }
        }
      });
    };

    reader.onerror = function (ev) {
      app.me.datasets.remove(dataset);
      app.message({
        text: 'File loading problem: ' + reader.error,
        type: 'error',
        error: reader.error
      });
    };

    reader.onprogress = function (ev) {
      if (ev.lengthComputable) {
        // ev.loaded and ev.total are ProgressEvent properties
        app.progress(parseInt(100.0 * ev.loaded / ev.total));
      }
    };

    reader.readAsText(uploadedFile);
  },
  exportSession: function () {
    var json = app.me.toJSON();

    if (app.me.sessionType === 'client') {
      // for client datasets, also save the data in the session file
      app.me.datasets.forEach(function (dataset, i) {
        json.datasets[i].data = dataset.data;
      });
    }
    var blob = new window.Blob([JSON.stringify(json)], {type: 'application/json'});
    var url = window.URL.createObjectURL(blob);

    var element = document.createElement('a');
    element.download = 'session.json';
    element.href = url;
    element.click();

    window.URL.revokeObjectURL(url);
  },
  exportData: function () {
    var chartsData = [];

    var partitionRankToName = {1: 'a', 2: 'b', 3: 'c', 4: 'd'};
    var aggregateRankToName = {1: 'aa', 2: 'bb', 3: 'cc', 4: 'dd', 5: 'ee'};

    app.me.dataview.filters.forEach(function (filter) {
      var map = {};
      var axis = [];
      filter.partitions.forEach(function (partition) {
        map[partitionRankToName[partition.rank]] = partition.facetName;
        axis.push(partition.facetName);
      });
      filter.aggregates.forEach(function (aggregate) {
        map[aggregateRankToName[aggregate.rank]] = aggregate.operation + ' ' + aggregate.facetName;
      });
      map['count'] = 'count';

      var data = [];
      filter.data.forEach(function (d) {
        var mapped = {};
        Object.keys(d).forEach(function (k) {
          if (map[k]) {
            mapped[map[k]] = d[k];
          }
        });
        data.push(mapped);
      });
      chartsData.push({
        chartType: filter.chartType,
        axis: axis.join(','),
        data: data
      });
    });

    var blob = new window.Blob([JSON.stringify(chartsData)], {type: 'application/json'});
    var url = window.URL.createObjectURL(blob);

    var element = document.createElement('a');
    element.download = 'data.json';
    element.href = url;
    element.click();

    window.URL.revokeObjectURL(url);
  },
  importLocalSession: function () {
    // var fileLoader = this.queryByHook('session-upload-input');
    var fileLoader = document.getElementById('sessionuploadBtn');
    var uploadedFile = fileLoader.files[0];
    var reader = new window.FileReader();

    reader.onload = function (ev) {
      var data = JSON.parse(ev.target.result);
      app.loadSessionBlob(data);
      app.message({
        text: 'Session "' + uploadedFile.name + '" was uploaded succesfully',
        type: 'ok'
      });
    };

    reader.onerror = function (ev) {
      app.message({
        text: 'Could not load Session "' + uploadedFile.name + '"',
        type: 'error',
        error: ev
      });
    };

    reader.readAsText(uploadedFile);
  },
  zenodoRequest: async function(zenodoParams) {

    var url_addition = zenodoParams.url_addition;
    var requestType = zenodoParams.requestType;
    var bodyData = zenodoParams.bodyData;
    // console.log('requestType:', requestType);

    var base_url = new URL("https://sandbox.zenodo.org/api/deposit/depositions");

    if (zenodoParams.base_url){
      base_url = zenodoParams.base_url;
    }

    var zenodoToken = process.env.ZENODO_TOKEN;
    if (url_addition) {
      // console.log(" Addition is provided: ", url_addition);
      base_url = base_url + "/" + url_addition;
    }
    var url = new URL(base_url),
    params = {
      access_token: zenodoToken
    };
    Object.keys(params).forEach(function(key){
      url.searchParams.append(key, params[key]);
    });

    // console.log('Zenodo base_url:', base_url);
    // console.log('Zenodo url:', url);

    var request_options = {};

    if (requestType === "doi") {
      request_options = {
        cache: "no-cache",
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(bodyData)
      }
    }
    else if (requestType === "upload") {
      request_options = {
        cache: "no-cache",
        method: "POST",
        body: bodyData
      }
    }
    else if (requestType === "publish") {
      request_options = {
        cache: "no-cache",
        method: "POST",
      }
    }
    else if (requestType === "meta") {
      request_options = {
        cache: "no-cache",
        method: "PUT",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(bodyData)
      }
    }
    else if (requestType === "download") {
      request_options = {
        cache: "no-cache",
        method: "GET",
        withCredentials: true,
      }
    }
    else {
      console.error('Unknown method');
    }

    // console.log('request_options: ', request_options);

    var response = await fetch(url, request_options);
    var data = await response.json();
    return data;
  }
});

/**
 * run it on domReady
 */
domReady(function () {
  app.init();

  if ( process.env.MODE === 'server' ) {
    console.log('connecting to database at', process.env.DB_SERVER + ":" + process.env.DB_SERVER_PORT);
    app.me.isLockedDown = true;
    app.me.connectToServer(process.env.DB_SERVER + ":" + process.env.DB_SERVER_PORT);
    app.me.socket.emit('getDatasets');
  }
});