<commit_before>/*    Copyright 2016-2021 Firewalla Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('./logger.js')(__filename);

const rclient = require('../util/redis_manager.js').getRedisClient()

const util = require('util');

const LogQuery = require('./LogQuery.js')

const IntelTool = require('../net2/IntelTool');
const intelTool = new IntelTool();

const auditTool = require('./AuditTool')

const MAX_RECENT_FLOW = 100;

const _ = require('lodash');

class FlowTool extends LogQuery {
  trimFlow(flow) {
    if(!flow)
      return;

    if("flows" in flow)
      delete flow.flows;

    if("pf" in flow)
      delete flow.pf;

    if("af" in flow)
      delete flow.af;

//    if("f" in flow)
//      delete flow.f;
    if("uids_array" in flow) {
      flow.uids = flow.uids_array.filter((v, i) => {
        return flow.uids_array.indexOf(v) === i;
      });
      delete flow.uids_array;
    }
  }

  mergeLog(targetFlow, flow) {
    targetFlow.download += flow.download;
    targetFlow.upload += flow.upload;
    targetFlow.duration += flow.duration;
    if (targetFlow.ts < flow.ts) { // ts had been converted to the _ts: update/record time
      targetFlow.ts = flow.ts;
    }
  }

  shouldMerge(targetFlow, flow) {
    if (targetFlow.type || flow.type) return auditTool.shouldMerge()
    const compareKeys = ['device', 'ip', 'fd', 'port', 'protocol'];
    return _.isEqual(_.pick(targetFlow, compareKeys), _.pick(flow, compareKeys));
  }

  includeFirewallaInterfaces() { return false }

  isLogValid(flow, options) {
    if (!super.isLogValid(flow, options)) return false

    let o = flow;

    if ( !('upload' in o) || !('download' in o) ) {
      return false
    }
    if (o.upload == 0 && o.download == 0) {
      // ignore zero length flows
      return false;
    }
    return true;
  }

  _enrichCountryInfo(flow) {
    let sh = flow.sh;
    let dh = flow.dh;
    let lh = flow.lh;

    if (sh === lh) {
      flow.country = intelTool.getCountry(dh)
    } else {
      flow.country = intelTool.getCountry(sh)
    }
  }

  async prepareRecentFlows(json, options) {
    log.verbose('prepareRecentFlows', JSON.stringify(options))
    options = options || {}
    if (!options.count || options.count > MAX_RECENT_FLOW) options.count = MAX_RECENT_FLOW
    if (!options.asc) options.asc = false;

    if (!("flows" in json)) {
      json.flows = {};
    }

    const feeds = []
    if (options.direction) {
      feeds.push({ query: this.getAllLogs.bind(this) })
    } else {
      feeds.push({ query: this.getAllLogs.bind(this), options: {direction: 'in'} })
      feeds.push({ query: this.getAllLogs.bind(this), options: {direction: 'out'} })
    }
    if (options.audit) {
      feeds.push({ query: auditTool.getAllLogs.bind(auditTool), options: {block: true} })
    }
    if (options.auditDNSSuccess) {
      feeds.push({ query: auditTool.getAllLogs.bind(auditTool), options: {block: false} })
    }
    delete options.audit
    delete options.auditDNSSuccess
    let recentFlows = await this.logFeeder(options, feeds)

    recentFlows = await this.enrichWithIntel(recentFlows.slice(0, options.count));

    json.flows.recent = recentFlows;
    log.verbose('prepareRecentFlows ends', JSON.stringify(options))
    return recentFlows
  }

  // convert flow json to a simplified json format that's more readable by app
  toSimpleFormat(flow) {
    let f = {
      ltype: 'flow'
    };
    f.ts = flow._ts; // _ts:update/record time, front-end always show up this
    f.fd = flow.fd;
    f.count = flow.ct || 1,
    f.duration = flow.du
    f.intf = flow.intf;
    f.tags = flow.tags;

    if(flow.mac) {
      f.device = flow.mac;
    }

    if (flow.rl) {
      // real IP:port of the client in VPN network
      f.rl = flow.rl;
    }

    f.protocol = flow.pr;

    try {
      if(flow.lh === flow.sh) {
        f.port = Number(flow.dp);
        f.devicePort = Number(flow.sp[0]);
      } else {
        f.port = Number(flow.sp[0]);
        f.devicePort = Number(flow.dp);
      }
    } catch(err) {
    }

    if(flow.lh === flow.sh) {
      f.ip = flow.dh;
      f.deviceIP = flow.sh;
      f.upload = flow.ob;
      f.download = flow.rb;
    } else {
      f.ip = flow.sh;
      f.deviceIP = flow.dh;
      f.upload = flow.rb;
      f.download = flow.ob;
    }

    return f;
  }

  _aggregateTransferBy10Min(results) {
    const aggrResults = {};
    results.forEach((x) => {
      const ts = x.ts;
      const tenminTS = Math.floor(Number(ts) / 600) * 600;
      if(!aggrResults[tenminTS]) {
        aggrResults[tenminTS] = {
          ts: tenminTS,
          ob: x.ob,
          rb: x.rb
        }
      } else {
        const old = aggrResults[tenminTS];
        aggrResults[tenminTS] = {
          ts: tenminTS,
          ob: x.ob + old.ob,
          rb: x.rb + old.rb
        }
      }
    })
    return Object.values(aggrResults).sort((x,y) => {
      if(x.ts > y.ts) {
        return 1;
      } else if(x.ts === y.ts) {
        return 0;
      } else {
        return -1;
      }
    });
  }

  async _getTransferTrend(target, destinationIP, options) {
    options = options || {};
    const end = options.end || Math.floor(new Date() / 1000);
    const begin = options.begin || end - 3600 * 6; // 6 hours
    const direction = options.direction || 'in';

    const key = util.format("flow:conn:%s:%s", direction, target);

    const results = await rclient.zrangebyscoreAsync([key, begin, end]);

    if(results === null || results.length === 0) {
      return [];
    }

    const list = results
      .map((jsonString) => {
        try {
          return JSON.parse(jsonString);
        } catch(err) {
          log.error(`Failed to parse json string: ${jsonString}, err: ${err}`);
          return null;
        }
      })
      .filter((x) => x !== null)
      .filter((x) => x.sh === destinationIP || x.dh === destinationIP)
      .map((x) => {
        return {
          ts: x.ts,
          ob: x.sh === destinationIP ? x.rb : x.ob, // ob stands for number of bytes transferred from local to remote, regardless of flow direction
          rb: x.sh === destinationIP ? x.ob : x.rb  // rb strands for number of bytes transferred from remote to local, regardless of flow direction
        }
      })

    return list;
  }

  async getTransferTrend(deviceMAC, destinationIP, options) {
    options = options || {};

    const transfers = [];

    if (!options.direction || options.direction === "in") {
      const optionsCopy = JSON.parse(JSON.stringify(options));
      optionsCopy.direction = "in";
      const t_in = await this._getTransferTrend(deviceMAC, destinationIP, optionsCopy);
      transfers.push.apply(transfers, t_in);
    }

    if (!options.direction || options.direction === "out") {
      const optionsCopy = JSON.parse(JSON.stringify(options));
      optionsCopy.direction = "out";
      const t_out = await this._getTransferTrend(deviceMAC, destinationIP, optionsCopy);
      transfers.push.apply(transfers, t_out);
    }
    return this._aggregateTransferBy10Min(transfers);
  }

  getLogKey(mac, options) {
    return util.format("flow:conn:%s:%s", options.direction, mac);
  }

  addFlow(mac, type, flow) {
    let key = this.getLogKey(mac, type);

    if(typeof flow !== 'object') {
      return Promise.reject("Invalid flow type: " + typeof flow);
    }

    return rclient.zaddAsync(key, flow.ts, JSON.stringify(flow));
  }

  removeFlow(mac, type, flow) {
    let key = this.getLogKey(mac, type);

    if(typeof flow !== 'object') {
      return Promise.reject("Invalid flow type: " + typeof flow);
    }

    return rclient.zremAsync(key, JSON.stringify(flow))
  }

  // lagacy api, returns raw redis data
  queryFlows(mac, type, begin, end) {
    let key = this.getLogKey(mac, {direction: type});

    return rclient.zrangebyscoreAsync(key, "(" + begin, end) // char '(' means open interval
      .then(flowStrings =>
        flowStrings.map(JSON.parse).filter(x => ('ob' in x) && ('rb' in x) && (x.ob != 0 || x.rb != 0))
      )
  }

  getDestIP(flow) {
    if(!flow) {
      return null
    }

    if(flow.lh === flow.sh) {
      return flow.dh;
    } else {
      return flow.sh;
    }
  }

  getDownloadTraffic(flow) {
    if(flow.lh === flow.sh) {
      return flow.rb;
    } else {
      return flow.ob;
    }
  }

  getUploadTraffic(flow) {
    if(flow.lh === flow.sh) {
      return flow.ob;
    } else {
      return flow.rb;
    }
  }
}

module.exports = new FlowTool();
<commit_msg>fix typo<commit_after>/*    Copyright 2016-2021 Firewalla Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('./logger.js')(__filename);

const rclient = require('../util/redis_manager.js').getRedisClient()

const util = require('util');

const LogQuery = require('./LogQuery.js')

const IntelTool = require('../net2/IntelTool');
const intelTool = new IntelTool();

const auditTool = require('./AuditTool')

const MAX_RECENT_FLOW = 100;

const _ = require('lodash');

class FlowTool extends LogQuery {
  trimFlow(flow) {
    if(!flow)
      return;

    if("flows" in flow)
      delete flow.flows;

    if("pf" in flow)
      delete flow.pf;

    if("af" in flow)
      delete flow.af;

//    if("f" in flow)
//      delete flow.f;
    if("uids_array" in flow) {
      flow.uids = flow.uids_array.filter((v, i) => {
        return flow.uids_array.indexOf(v) === i;
      });
      delete flow.uids_array;
    }
  }

  mergeLog(targetFlow, flow) {
    targetFlow.download += flow.download;
    targetFlow.upload += flow.upload;
    targetFlow.duration += flow.duration;
    if (targetFlow.ts < flow.ts) { // ts had been converted to the _ts: update/record time
      targetFlow.ts = flow.ts;
    }
  }

  shouldMerge(targetFlow, flow) {
    if (targetFlow.type || flow.type) return auditTool.shouldMerge()
    const compareKeys = ['device', 'ip', 'fd', 'port', 'protocol'];
    return _.isEqual(_.pick(targetFlow, compareKeys), _.pick(flow, compareKeys));
  }

  includeFirewallaInterfaces() { return false }

  isLogValid(flow, options) {
    if (!super.isLogValid(flow, options)) return false

    let o = flow;

    if ( !('upload' in o) || !('download' in o) ) {
      return false
    }
    if (o.upload == 0 && o.download == 0) {
      // ignore zero length flows
      return false;
    }
    return true;
  }

  _enrichCountryInfo(flow) {
    let sh = flow.sh;
    let dh = flow.dh;
    let lh = flow.lh;

    if (sh === lh) {
      flow.country = intelTool.getCountry(dh)
    } else {
      flow.country = intelTool.getCountry(sh)
    }
  }

  async prepareRecentFlows(json, options) {
    log.verbose('prepareRecentFlows', JSON.stringify(options))
    options = options || {}
    if (!options.count || options.count > MAX_RECENT_FLOW) options.count = MAX_RECENT_FLOW
    if (!options.asc) options.asc = false;

    if (!("flows" in json)) {
      json.flows = {};
    }

    const feeds = []
    if (options.direction) {
      feeds.push({ query: this.getAllLogs.bind(this) })
    } else {
      feeds.push({ query: this.getAllLogs.bind(this), options: {direction: 'in'} })
      feeds.push({ query: this.getAllLogs.bind(this), options: {direction: 'out'} })
    }
    if (options.audit) {
      feeds.push({ query: auditTool.getAllLogs.bind(auditTool), options: {block: true} })
    }
    if (options.auditDNSSuccess) {
      feeds.push({ query: auditTool.getAllLogs.bind(auditTool), options: {block: false} })
    }
    delete options.audit
    delete options.auditDNSSuccess
    let recentFlows = await this.logFeeder(options, feeds)

    recentFlows = await this.enrichWithIntel(recentFlows.slice(0, options.count));

    json.flows.recent = recentFlows;
    log.verbose('prepareRecentFlows ends', JSON.stringify(options))
    return recentFlows
  }

  // convert flow json to a simplified json format that's more readable by app
  toSimpleFormat(flow) {
    let f = {
      ltype: 'flow'
    };
    f.ts = flow._ts; // _ts:update/record time, front-end always show up this
    f.fd = flow.fd;
    f.count = flow.ct || 1,
    f.duration = flow.du
    f.intf = flow.intf;
    f.tags = flow.tags;

    if(flow.mac) {
      f.device = flow.mac;
    }

    if (flow.rl) {
      // real IP:port of the client in VPN network
      f.rl = flow.rl;
    }

    f.protocol = flow.pr;

    try {
      if(flow.lh === flow.sh) {
        f.port = Number(flow.dp);
        f.devicePort = Number(flow.sp[0]);
      } else {
        f.port = Number(flow.sp[0]);
        f.devicePort = Number(flow.dp);
      }
    } catch(err) {
    }

    if(flow.lh === flow.sh) {
      f.ip = flow.dh;
      f.deviceIP = flow.sh;
      f.upload = flow.ob;
      f.download = flow.rb;
    } else {
      f.ip = flow.sh;
      f.deviceIP = flow.dh;
      f.upload = flow.rb;
      f.download = flow.ob;
    }

    return f;
  }

  _aggregateTransferBy10Min(results) {
    const aggrResults = {};
    results.forEach((x) => {
      const ts = x.ts;
      const tenminTS = Math.floor(Number(ts) / 600) * 600;
      if(!aggrResults[tenminTS]) {
        aggrResults[tenminTS] = {
          ts: tenminTS,
          ob: x.ob,
          rb: x.rb
        }
      } else {
        const old = aggrResults[tenminTS];
        aggrResults[tenminTS] = {
          ts: tenminTS,
          ob: x.ob + old.ob,
          rb: x.rb + old.rb
        }
      }
    })
    return Object.values(aggrResults).sort((x,y) => {
      if(x.ts > y.ts) {
        return 1;
      } else if(x.ts === y.ts) {
        return 0;
      } else {
        return -1;
      }
    });
  }

  async _getTransferTrend(target, destinationIP, options) {
    options = options || {};
    const end = options.end || Math.floor(new Date() / 1000);
    const begin = options.begin || end - 3600 * 6; // 6 hours
    const direction = options.direction || 'in';

    const key = util.format("flow:conn:%s:%s", direction, target);

    const results = await rclient.zrangebyscoreAsync([key, begin, end]);

    if(results === null || results.length === 0) {
      return [];
    }

    const list = results
      .map((jsonString) => {
        try {
          return JSON.parse(jsonString);
        } catch(err) {
          log.error(`Failed to parse json string: ${jsonString}, err: ${err}`);
          return null;
        }
      })
      .filter((x) => x !== null)
      .filter((x) => x.sh === destinationIP || x.dh === destinationIP)
      .map((x) => {
        return {
          ts: x.ts,
          ob: x.sh === destinationIP ? x.rb : x.ob, // ob stands for number of bytes transferred from local to remote, regardless of flow direction
          rb: x.sh === destinationIP ? x.ob : x.rb  // rb strands for number of bytes transferred from remote to local, regardless of flow direction
        }
      })

    return list;
  }

  async getTransferTrend(deviceMAC, destinationIP, options) {
    options = options || {};

    const transfers = [];

    if (!options.direction || options.direction === "in") {
      const optionsCopy = JSON.parse(JSON.stringify(options));
      optionsCopy.direction = "in";
      const t_in = await this._getTransferTrend(deviceMAC, destinationIP, optionsCopy);
      transfers.push.apply(transfers, t_in);
    }

    if (!options.direction || options.direction === "out") {
      const optionsCopy = JSON.parse(JSON.stringify(options));
      optionsCopy.direction = "out";
      const t_out = await this._getTransferTrend(deviceMAC, destinationIP, optionsCopy);
      transfers.push.apply(transfers, t_out);
    }
    return this._aggregateTransferBy10Min(transfers);
  }

  getLogKey(mac, options) {
    return util.format("flow:conn:%s:%s", options.direction, mac);
  }

  addFlow(mac, type, flow) {
    let key = this.getLogKey(mac, type);

    if(typeof flow !== 'object') {
      return Promise.reject("Invalid flow type: " + typeof flow);
    }

    return rclient.zaddAsync(key, flow.ts, JSON.stringify(flow));
  }

  removeFlow(mac, type, flow) {
    let key = this.getLogKey(mac, type);

    if(typeof flow !== 'object') {
      return Promise.reject("Invalid flow type: " + typeof flow);
    }

    return rclient.zremAsync(key, JSON.stringify(flow))
  }

  // legacy api, returns raw redis data
  queryFlows(mac, type, begin, end) {
    let key = this.getLogKey(mac, {direction: type});

    return rclient.zrangebyscoreAsync(key, "(" + begin, end) // char '(' means open interval
      .then(flowStrings =>
        flowStrings.map(JSON.parse).filter(x => ('ob' in x) && ('rb' in x) && (x.ob != 0 || x.rb != 0))
      )
  }

  getDestIP(flow) {
    if(!flow) {
      return null
    }

    if(flow.lh === flow.sh) {
      return flow.dh;
    } else {
      return flow.sh;
    }
  }

  getDownloadTraffic(flow) {
    if(flow.lh === flow.sh) {
      return flow.rb;
    } else {
      return flow.ob;
    }
  }

  getUploadTraffic(flow) {
    if(flow.lh === flow.sh) {
      return flow.ob;
    } else {
      return flow.rb;
    }
  }
}

module.exports = new FlowTool();
<|endoftext|>


<commit_before>'use strict';

const MVA = require('@scola/mva');

class SigninPasswordView extends MVA.View.Abstract {
  constructor(model, i18n) {
    super();

    this.model = model;
    this.i18n = i18n;
    this.string = i18n.get('string');
    this.popup = null;
  }

  build() {
    this
      .view('form', '@scola.form')
      .element('form')
      .style({
        background: '#EEE',
        position: 'absolute',
        width: '23.75em',
        height: '8.5em'
      })
      .listen({
        'submit': this.handleSubmit
      }, this)
      .element('fieldset')
      .style({
        bottom: '1em',
        left: '1em',
        right: '1em',
        top: '1em'
      });

    this
      .view('username', '@scola.form.input')
      .element('wrapper')
      .style('@scola.absmax')
      .element('input')
      .properties({
        type: 'text',
        name: 'username'
      });

    this
      .view('password', '@scola.form.input')
      .element('wrapper')
      .style('@scola.absmax')
      .style({
        right: '3em',
        top: '3em'
      })
      .element('input')
      .properties({
        type: 'password',
        name: 'password'
      });

    this
      .view('button', '@scola.form.button-icon')
      .element('button')
      .properties({
        type: 'submit'
      })
      .style({
        position: 'absolute',
        right: 0,
        top: '3em'
      })
      .listen({
        'enable': this.handleEnable
      }, this)
      .element('icon')
      .properties({
        className: 'ion ion-android-arrow-forward'
      });

    this
      .view('checkbox', '@scola.form.checkbox-label')
      .element('wrapper')
      .style('@scola.absmax')
      .style({
        height: '1.5em',
        top: '6em'
      })
      .element('checkbox')
      .listen({
        'check': this.handleCheck
      }, this);

    this
      .view('form')
      .element('fieldset')
      .append(
        this.view('username').render()
      )
      .append(
        this.view('password').render()
      )
      .append(
        this.view('button').render()
      )
      .append(
        this.view('checkbox').render()
      );

    this.view('popups', '@scola.popup.container');

    this.bindListeners();
    this.handleLanguageChange();

    return this;
  }

  render() {
    return this.view('form').render();
  }

  destroy() {
    this.unbindListeners();
    super.destroy();
  }

  bindListeners() {
    this.bindListener('language', this.i18n, this.handleLanguageChange);
    this.bindListener('change', this.model, this.handleModelChange);
    this.bindListener('error', this.model, this.handleModelError);
  }

  unbindListeners() {
    this.unbindListener('language', this.i18n, this.handleLanguageChange);
    this.unbindListener('change', this.model, this.handleModelChange);
    this.unbindListener('error', this.model, this.handleModelError);
  }

  handleSubmit(event) {
    event.preventDefault();

    if (this.bubble) {
      this.bubble.destroy(true);
    }

    if (!this.model.isIdle()) {
      return;
    }

    this
      .model
      .setBusy()
      .setPermanent(this.view('checkbox').checked())
      .setUsername(this.view('username').value())
      .setPassword(this.view('password').value());

    this.changeButton();

    if (this.model.commit('signin-password-begin')) {
      this.model.signinPassword();
    }
  }

  handleEnable(event) {
    event
      .view
      .element('icon')
      .style({
        display: event.enabled ? '' : 'none'
      });

    if (event.enabled && this.hasView('spinner')) {
      this
        .removeView('spinner')
        .destroy();

      this
        .view('button')
        .element('button')
        .properties({
          title: 'Inloggen'
        })
        .style({
          cursor: 'pointer'
        });
    } else if (!event.enabled && !this.hasView('spinner')) {
      this
        .view('spinner', '@scola.widget.spinner');

      this
        .view('button')
        .element('button')
        .properties({
          title: 'Laden...'
        })
        .style({
          cursor: 'default'
        })
        .append(
          this.view('spinner').render()
        );
    }
  }

  handleCheck(event) {
    event
      .view
      .element('checkbox')
      .properties({
        className: event.checked ? 'ion ion-checkmark' : ''
      });
  }

  handleLanguageChange() {
    this
      .view('username')
      .element('input')
      .properties({
        placeholder: this.string.format('@scola.auth.input.username')
      });

    this
      .view('password')
      .element('input')
      .properties({
        placeholder: this.string.format('@scola.auth.input.password')
      });

    this
      .view('button')
      .element('button')
      .properties({
        title: this.string.format('@scola.auth.input.sign-in')
      });

    this
      .view('checkbox')
      .element('label')
      .properties({
        innerHTML: this.string.format('@scola.auth.input.keep-signed-in')
      });
  }

  handleModelChange(event) {
    if (event.name === 'signin-password-end') {
      this.changeButton();
    }
  }

  handleModelError(event) {
    this.changeButton();

    if (event.error.message === '@scola.mva.error.commit') {
      return this.handleCommitError(event);
    }

    this.handleError(event);
  }

  handleCommitError(event) {
    this.model.setIdle();

    let error = null;
    let viewName = null;

    if (event.data.username instanceof Error) {
      error = event.data.username.message.split(' ');
      viewName = 'username';
    } else if (event.data.password instanceof Error) {
      error = event.data.password.message.split(' ');
      viewName = 'password';
    }

    if (viewName) {
      this.bubble = this
        .view(viewName)
        .view('bubble', '@scola.widget.tooltip');

      this
        .bubble
        .element('wrapper')
        .listen({
          destroy: () => {
            this.view(viewName).removeView('bubble');
          }
        })
        .element('text')
        .properties({
          innerHTML: this.string.format(...error)
        });

      this
        .view(viewName)
        .element('wrapper')
        .append(
          this.bubble.render()
        )
        .element('input')
        .get()
        .focus();

      this
        .bubble
        .bottom();
    }
  }

  handleError(event) {
    this.popup = this
      .view('popups')
      .view('popup', '@scola.popup');

    this
      .popup
      .element('title')
      .properties({
        innerHTML: this.string.format('@scola.mva.generic.error')
      })
      .element('text')
      .properties({
        innerHTML: this.string.format(event.error.message)
      });

    this
      .popup
      .view('button-ok', '@scola.popup.button')
      .element('button')
      .properties({
        innerHTML: this.string.format('@scola.mva.generic.ok')
      })
      .style({
        borderLeft: '1px solid #CCC',
        fontWeight: 'bold',
        width: '100%'
      })
      .listen({
        click: () => {
          this
            .view('popups')
            .removeView('popup')
            .destroy();
        }
      }, this);

    this
      .popup
      .element('wrapper')
      .listen({
        destroy: this.handleDestroyPopup
      }, this)
      .append(
        this.popup.view('button-ok').render()
      );

    this
      .view('popups')
      .element('container')
      .append(
        this.popup.render()
      );

    this
      .popup
      .center()
      .view('button-ok')
      .element('button')
      .get()
      .focus();
  }

  handleDestroyPopup() {
    this.model.setIdle();
    this.popup = null;
  }

  changeButton() {
    this
      .view('button')
      .enable(!this.model.isBusy());
  }
}

module.exports = SigninPasswordView;
<commit_msg>Fix bug: reference to error must include namespace<commit_after>'use strict';

const MVA = require('@scola/mva');

class SigninPasswordView extends MVA.View.Abstract {
  constructor(model, i18n) {
    super();

    this.model = model;
    this.i18n = i18n;
    this.string = i18n.get('string');
    this.popup = null;
  }

  build() {
    this
      .view('form', '@scola.form')
      .element('form')
      .style({
        background: '#EEE',
        position: 'absolute',
        width: '23.75em',
        height: '8.5em'
      })
      .listen({
        'submit': this.handleSubmit
      }, this)
      .element('fieldset')
      .style({
        bottom: '1em',
        left: '1em',
        right: '1em',
        top: '1em'
      });

    this
      .view('username', '@scola.form.input')
      .element('wrapper')
      .style('@scola.absmax')
      .element('input')
      .properties({
        type: 'text',
        name: 'username'
      });

    this
      .view('password', '@scola.form.input')
      .element('wrapper')
      .style('@scola.absmax')
      .style({
        right: '3em',
        top: '3em'
      })
      .element('input')
      .properties({
        type: 'password',
        name: 'password'
      });

    this
      .view('button', '@scola.form.button-icon')
      .element('button')
      .properties({
        type: 'submit'
      })
      .style({
        position: 'absolute',
        right: 0,
        top: '3em'
      })
      .listen({
        'enable': this.handleEnable
      }, this)
      .element('icon')
      .properties({
        className: 'ion ion-android-arrow-forward'
      });

    this
      .view('checkbox', '@scola.form.checkbox-label')
      .element('wrapper')
      .style('@scola.absmax')
      .style({
        height: '1.5em',
        top: '6em'
      })
      .element('checkbox')
      .listen({
        'check': this.handleCheck
      }, this);

    this
      .view('form')
      .element('fieldset')
      .append(
        this.view('username').render()
      )
      .append(
        this.view('password').render()
      )
      .append(
        this.view('button').render()
      )
      .append(
        this.view('checkbox').render()
      );

    this.view('popups', '@scola.popup.container');

    this.bindListeners();
    this.handleLanguageChange();

    return this;
  }

  render() {
    return this.view('form').render();
  }

  destroy() {
    this.unbindListeners();
    super.destroy();
  }

  bindListeners() {
    this.bindListener('language', this.i18n, this.handleLanguageChange);
    this.bindListener('change', this.model, this.handleModelChange);
    this.bindListener('error', this.model, this.handleModelError);
  }

  unbindListeners() {
    this.unbindListener('language', this.i18n, this.handleLanguageChange);
    this.unbindListener('change', this.model, this.handleModelChange);
    this.unbindListener('error', this.model, this.handleModelError);
  }

  handleSubmit(event) {
    event.preventDefault();

    if (this.bubble) {
      this.bubble.destroy(true);
    }

    if (!this.model.isIdle()) {
      return;
    }

    this
      .model
      .setBusy()
      .setPermanent(this.view('checkbox').checked())
      .setUsername(this.view('username').value())
      .setPassword(this.view('password').value());

    this.changeButton();

    if (this.model.commit('signin-password-begin')) {
      this.model.signinPassword();
    }
  }

  handleEnable(event) {
    event
      .view
      .element('icon')
      .style({
        display: event.enabled ? '' : 'none'
      });

    if (event.enabled && this.hasView('spinner')) {
      this
        .removeView('spinner')
        .destroy();

      this
        .view('button')
        .element('button')
        .properties({
          title: 'Inloggen'
        })
        .style({
          cursor: 'pointer'
        });
    } else if (!event.enabled && !this.hasView('spinner')) {
      this
        .view('spinner', '@scola.widget.spinner');

      this
        .view('button')
        .element('button')
        .properties({
          title: 'Laden...'
        })
        .style({
          cursor: 'default'
        })
        .append(
          this.view('spinner').render()
        );
    }
  }

  handleCheck(event) {
    event
      .view
      .element('checkbox')
      .properties({
        className: event.checked ? 'ion ion-checkmark' : ''
      });
  }

  handleLanguageChange() {
    this
      .view('username')
      .element('input')
      .properties({
        placeholder: this.string.format('@scola.auth.input.username')
      });

    this
      .view('password')
      .element('input')
      .properties({
        placeholder: this.string.format('@scola.auth.input.password')
      });

    this
      .view('button')
      .element('button')
      .properties({
        title: this.string.format('@scola.auth.input.sign-in')
      });

    this
      .view('checkbox')
      .element('label')
      .properties({
        innerHTML: this.string.format('@scola.auth.input.keep-signed-in')
      });
  }

  handleModelChange(event) {
    if (event.name === 'signin-password-end') {
      this.changeButton();
    }
  }

  handleModelError(event) {
    this.changeButton();

    if (event.error.message === '@scola.mva.error.commit') {
      return this.handleCommitError(event);
    }

    this.handleError(event);
  }

  handleCommitError(event) {
    this.model.setIdle();

    let error = null;
    let viewName = null;

    ['username', 'password'].forEach((key) => {
      if (!error && event.data['@scola.auth.' + key] instanceof Error) {
        error = event.data['@scola.auth.' + key].message.split(' ');
        viewName = key;
      }
    });

    if (viewName) {
      this.bubble = this
        .view(viewName)
        .view('bubble', '@scola.widget.tooltip');

      this
        .bubble
        .element('wrapper')
        .listen({
          destroy: () => {
            this.view(viewName).removeView('bubble');
          }
        })
        .element('text')
        .properties({
          innerHTML: this.string.format(...error)
        });

      this
        .view(viewName)
        .element('wrapper')
        .append(
          this.bubble.render()
        )
        .element('input')
        .get()
        .focus();

      this
        .bubble
        .bottom();
    }
  }

  handleError(event) {
    this.popup = this
      .view('popups')
      .view('popup', '@scola.popup');

    this
      .popup
      .element('title')
      .properties({
        innerHTML: this.string.format('@scola.mva.generic.error')
      })
      .element('text')
      .properties({
        innerHTML: this.string.format(event.error.message)
      });

    this
      .popup
      .view('button-ok', '@scola.popup.button')
      .element('button')
      .properties({
        innerHTML: this.string.format('@scola.mva.generic.ok')
      })
      .style({
        borderLeft: '1px solid #CCC',
        fontWeight: 'bold',
        width: '100%'
      })
      .listen({
        click: () => {
          this
            .view('popups')
            .removeView('popup')
            .destroy();
        }
      }, this);

    this
      .popup
      .element('wrapper')
      .listen({
        destroy: this.handleDestroyPopup
      }, this)
      .append(
        this.popup.view('button-ok').render()
      );

    this
      .view('popups')
      .element('container')
      .append(
        this.popup.render()
      );

    this
      .popup
      .center()
      .view('button-ok')
      .element('button')
      .get()
      .focus();
  }

  handleDestroyPopup() {
    this.model.setIdle();
    this.popup = null;
  }

  changeButton() {
    this
      .view('button')
      .enable(!this.model.isBusy());
  }
}

module.exports = SigninPasswordView;
<|endoftext|>


<commit_before>/**
 Copyright 2014 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.
 */

var truncate = function (str, width) {
    if (str && str.length > width) {
        return str.slice(0, width) + "...";
    }
    return str;
};

var pods = [];
var services = [];
var controllers = [];
var uses = {};

var groups = {};

var insertByName = function (index, value) {

    if (!value || !value.metadata.labels || value.metadata.name == 'kubernetes') {
        return;
    }
    var list = groups[value.metadata.labels.run];
    if (!list) {
        list = [];
        groups[value.metadata.labels.run] = list;
    }
    list.push(value);
};

var groupByName = function () {
    $.each(pods.items, insertByName);
    $.each(controllers.items, insertByName);
    $.each(services.items, insertByName);
};

var matchesLabelQuery = function (labels, selector) {
    var match = true;

    if(!labels) { return false; }

    $.each(selector, function (key, value) {
        if (labels[key] !== value) {
            match = false;
        }
    });
    return match;
};

var connectControllers = function () {
    //connectUses();
    for (var i = 0; i < controllers.items.length; i++) {
        var controller = controllers.items[i];
        for (var j = 0; j < pods.items.length; j++) {
            var pod = pods.items[j];
            if (pod.metadata.labels && controller.metadata && controller.metadata.labels && controller.metadata.labels.run &&
                pod.metadata.labels['run'] == controller.metadata.labels.run) {
                jsPlumb.connect({
                    source: controller.metadata.uid,
                    target: pod.metadata.uid,
                    anchors: ["Bottom", "Bottom"],
                    paintStyle: {lineWidth: 5, strokeStyle: 'rgb(51,105,232)'},
                    joinStyle: "round",
                    endpointStyle: {fillStyle: 'rgb(51,105,232)', radius: 7},
                    connector: ["Flowchart", {cornerRadius: 5}]
                });
            }
        }
    }

    for (var i = 0; i < services.items.length; i++) {
        var service = services.items[i];
        if (service.metadata.name == 'kubernetes') {
            continue;
        }

        for (var j = 0; j < pods.items.length; j++) {

            var pod = pods.items[j];
            if (matchesLabelQuery(pod.metadata.labels, service.spec.selector)) {
                jsPlumb.connect(
                    {
                        source: service.metadata.uid,
                        target: pod.metadata.uid,
                        anchors: ["Bottom", "Top"],
                        paintStyle: {lineWidth: 5, strokeStyle: 'rgb(0,153,57)'},
                        endpointStyle: {fillStyle: 'rgb(0,153,57)', radius: 7},
                        joinStyle: "round",
                        connector: ["Flowchart", {cornerRadius: 5}]
                    });
            }
        }
    }
};

var colors = [
    'rgb(213,15,37)',
    'rgba(238,178,17,1.0)'
];

var connectUses = function () {
    var colorIx = 0;

    $.each(uses, function (key, list) {

        var color = colors[colorIx];
        colorIx++;
        $.each(pods.items, function (i, pod) {

            if (pod.metadata.labels && pod.metadata.labels.run == key) {
                $.each(list, function (j, serviceId) {
                    jsPlumb.connect(
                        {
                            source: pod.metadata.uid,
                            target: serviceId,
                            endpoint: "Blank",
                            anchors: ["Bottom", "Top"],
                            connector: "Straight",
                            paintStyle: {lineWidth: 5, strokeStyle: color},
                            overlays: [
                                ["Arrow", {width: 15, length: 30, location: 0.3}],
                                ["Arrow", {width: 15, length: 30, location: 0.6}],
                                ["Arrow", {width: 15, length: 30, location: 1}],
                            ],
                        });
                });
            }
        });
    });
};

var makeGroupOrder = function () {
    var groupScores = {};
    $.each(uses, function (key, value) {
        if (!groupScores[key]) {
            groupScores[key] = 0;
        }
        $.each(value, function (ix, uses) {
            if (!groupScores[uses]) {
                groupScores[uses] = 1;
            } else {
                groupScores[uses]++;
            }
        });
    });
    $.each(groups, function(key, value) {
      if (!groupScores[key]) {
        groupScores[key] = 0;
      }
    });
    var groupOrder = [];
    $.each(groupScores, function (key, value) {
        groupOrder.push(key);
    });
    groupOrder.sort(function (a, b) {
        return groupScores[a] - groupScores[b];
    });
    return groupOrder;
};

var renderGroups = function () {
    var elt = $('#sheet');
    var y = 10;
    var serviceLeft = 0;
    var groupOrder = makeGroupOrder();
    $.each(groupOrder, function (ix, key) {
        list = groups[key];
        if (!list) {
            return;
        }
        var div = $('
');
        var x = 100;
        $.each(list, function (index, value) {
            var eltDiv = null;
            if (value.type == "pod") {
                eltDiv = $('
');
                eltDiv.text(truncate(value.metadata.name, 8));
            } else if (value.type == "service") {
                eltDiv = $('
');
                eltDiv.text(truncate(value.metadata.name, 20));
            } else {
                eltDiv = $('
');
                eltDiv.text(truncate(value.metadata.name, 20));
            }
            div.append(eltDiv);
            x += 130;
        });
        y += 400;
        serviceLeft += 200;
        elt.append(div);
    });
};

var insertUse = function (name, use) {
    for (var i = 0; i < uses[name].length; i++) {
        if (uses[name][i] == use) {
            return;
        }
    }
    uses[name].push(use);
};

var loadData = function () {
    var deferred = new $.Deferred();
    var req1 = $.getJSON("/api/v1/namespaces/default/pods", function (data) {
        pods = data;
        $.each(data.items, function (key, val) {
            val.type = 'pod';

            if (val.metadata.labels) {
                if (val.metadata.labels.uses) {
                    if (!uses[val.metadata.labels.run]) {
                        uses[val.metadata.labels.run] = val.metadata.labels.uses.split(",");
                    } else {
                        $.each(val.metadata.labels.uses.split(","), function (ix, use) {
                            insertUse(val.metadata.labels.run, use);
                        });
                    }
                }
            }
        });
    });

    var req2 = $.getJSON("/api/v1/namespaces/default/replicationcontrollers", function (data) {
        controllers = data;
        $.each(data.items, function (key, val) {
            val.type = 'replicationController';
        });
    });

    var req3 = $.getJSON("/api/v1/namespaces/default/services", function (data) {
        services = data;
        $.each(data.items, function (key, val) {
            val.type = 'service';
        });
    });
    $.when(req1, req2, req3).then(function () {
        deferred.resolve();
    });
    return deferred;
}

jsPlumb.bind("ready", function () {
    var instance = jsPlumb.getInstance({
        // default drag options
        DragOptions: {cursor: 'pointer', zIndex: 2000},
        // the overlays to decorate each connection with.  note that the label overlay uses a function to generate the label text; in this
        // case it returns the 'labelText' member that we set on each connection in the 'init' method below.
        ConnectionOverlays: [
            ["Arrow", {location: 1}],
            //[ "Label", {
            //	location:0.1,
            //	id:"label",
            //	cssClass:"aLabel"
            //}]
        ],
        Container: "flowchart-demo"
    });
    var promise = loadData();
    $.when(promise).then(function () {
        groupByName();
        renderGroups();
        connectControllers();
    })
    jsPlumb.fire("jsPlumbDemoLoaded", instance);
});
<commit_msg>Fix service connections<commit_after>/**
 Copyright 2014 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.
 */

var truncate = function (str, width) {
    if (str && str.length > width) {
        return str.slice(0, width) + "...";
    }
    return str;
};

var pods = [];
var services = [];
var controllers = [];
var uses = {};

var groups = {};

var insertByName = function (index, value) {

    if (!value || !value.metadata.labels || value.metadata.name == 'kubernetes') {
        return;
    }
    var list = groups[value.metadata.labels.run];
    if (!list) {
        list = [];
        groups[value.metadata.labels.run] = list;
    }
    list.push(value);
};

var groupByName = function () {
    $.each(pods.items, insertByName);
    $.each(controllers.items, insertByName);
    $.each(services.items, insertByName);
};

var matchesLabelQuery = function (labels, selector) {
    var match = true;

    if(!labels) { return false; }

    $.each(selector, function (key, value) {
        if (labels[key] !== value) {
            match = false;
        }
    });
    return match;
};

var connectControllers = function () {
    connectUses();
    for (var i = 0; i < controllers.items.length; i++) {
        var controller = controllers.items[i];
        for (var j = 0; j < pods.items.length; j++) {
            var pod = pods.items[j];
            if (pod.metadata.labels && controller.metadata && controller.metadata.labels && controller.metadata.labels.run &&
                pod.metadata.labels['run'] == controller.metadata.labels.run) {
                jsPlumb.connect({
                    source: controller.metadata.uid,
                    target: pod.metadata.uid,
                    anchors: ["Bottom", "Bottom"],
                    paintStyle: {lineWidth: 5, strokeStyle: 'rgb(51,105,232)'},
                    joinStyle: "round",
                    endpointStyle: {fillStyle: 'rgb(51,105,232)', radius: 7},
                    connector: ["Flowchart", {cornerRadius: 5}]
                });
            }
        }
    }

    for (var i = 0; i < services.items.length; i++) {
        var service = services.items[i];
        if (service.metadata.name == 'kubernetes') {
            continue;
        }

        for (var j = 0; j < pods.items.length; j++) {

            var pod = pods.items[j];

            if (matchesLabelQuery(pod.metadata.labels, service.spec.selector)) {
                jsPlumb.connect(
                    {
                        source: service.metadata.uid,
                        target: pod.metadata.uid,
                        anchors: ["Bottom", "Top"],
                        paintStyle: {lineWidth: 5, strokeStyle: 'rgb(0,153,57)'},
                        endpointStyle: {fillStyle: 'rgb(0,153,57)', radius: 7},
                        joinStyle: "round",
                        connector: ["Flowchart", {cornerRadius: 5}]
                    });
            }
        }
    }
};

var colors = [
    'rgb(213,15,37)',
    'rgba(238,178,17,1.0)'
];

var connectUses = function () {
    var colorIx = 0;

    $.each(uses, function (key, list) {

        var color = colors[colorIx];
        colorIx++;
        $.each(pods.items, function (i, pod) {
            if (pod.metadata.labels && pod.metadata.labels.run == key) {
                $.each(list, function (j, serviceKey) {
                  $.each(services.items, function (j, service) {
                    if (service.metadata.labels && service.metadata.labels.run == serviceKey) {
                        jsPlumb.connect(
                            {
                                source: pod.metadata.uid,
                                target: service.metadata.uid,
                                endpoint: "Blank",
                                anchors: ["Bottom", "Top"],
                                connector: "Straight",
                                paintStyle: {lineWidth: 5, strokeStyle: color},
                                overlays: [
                                    ["Arrow", {width: 15, length: 30, location: 0.3}],
                                    ["Arrow", {width: 15, length: 30, location: 0.6}],
                                    ["Arrow", {width: 15, length: 30, location: 1}],
                                ],
                            });
                    }
                  });
                });
            }
        });
    });
};

var makeGroupOrder = function () {
    var groupScores = {};
    $.each(uses, function (key, value) {
        if (!groupScores[key]) {
            groupScores[key] = 0;
        }
        $.each(value, function (ix, uses) {
            if (!groupScores[uses]) {
                groupScores[uses] = 1;
            } else {
                groupScores[uses]++;
            }
        });
    });
    $.each(groups, function(key, value) {
      if (!groupScores[key]) {
        groupScores[key] = 0;
      }
    });
    var groupOrder = [];
    $.each(groupScores, function (key, value) {
        groupOrder.push(key);
    });
    groupOrder.sort(function (a, b) {
        return groupScores[a] - groupScores[b];
    });
    return groupOrder;
};

var renderGroups = function () {
    var elt = $('#sheet');
    var y = 10;
    var serviceLeft = 0;
    var groupOrder = makeGroupOrder();
    $.each(groupOrder, function (ix, key) {
        list = groups[key];
        if (!list) {
            return;
        }
        var div = $('
');
        var x = 100;
        $.each(list, function (index, value) {
            var eltDiv = null;
            if (value.type == "pod") {
                eltDiv = $('
');
                eltDiv.text(truncate(value.metadata.name, 8));
            } else if (value.type == "service") {
                eltDiv = $('
');
                eltDiv.text(truncate(value.metadata.name, 20));
            } else {
                eltDiv = $('
');
                eltDiv.text(truncate(value.metadata.name, 20));
            }
            div.append(eltDiv);
            x += 130;
        });
        y += 400;
        serviceLeft += 200;
        elt.append(div);
    });
};

var insertUse = function (name, use) {
    for (var i = 0; i < uses[name].length; i++) {
        if (uses[name][i] == use) {
            return;
        }
    }
    uses[name].push(use);
};

var loadData = function () {
    var deferred = new $.Deferred();
    var req1 = $.getJSON("/api/v1/namespaces/default/pods", function (data) {
        pods = data;
        $.each(data.items, function (key, val) {
            val.type = 'pod';

            if (val.metadata.labels) {
                if (val.metadata.labels.uses) {
                    if (!uses[val.metadata.labels.run]) {
                        uses[val.metadata.labels.run] = val.metadata.labels.uses.split(",");
                    } else {
                        $.each(val.metadata.labels.uses.split(","), function (ix, use) {
                            insertUse(val.metadata.labels.run, use);
                        });
                    }
                }
            }
        });
    });

    var req2 = $.getJSON("/api/v1/namespaces/default/replicationcontrollers", function (data) {
        controllers = data;
        $.each(data.items, function (key, val) {
            val.type = 'replicationController';
        });
    });

    var req3 = $.getJSON("/api/v1/namespaces/default/services", function (data) {
        services = data;
        $.each(data.items, function (key, val) {
            val.type = 'service';
        });
    });
    $.when(req1, req2, req3).then(function () {
        deferred.resolve();
    });
    return deferred;
}

jsPlumb.bind("ready", function () {
    var instance = jsPlumb.getInstance({
        // default drag options
        DragOptions: {cursor: 'pointer', zIndex: 2000},
        // the overlays to decorate each connection with.  note that the label overlay uses a function to generate the label text; in this
        // case it returns the 'labelText' member that we set on each connection in the 'init' method below.
        ConnectionOverlays: [
            ["Arrow", {location: 1}],
            //[ "Label", {
            //	location:0.1,
            //	id:"label",
            //	cssClass:"aLabel"
            //}]
        ],
        Container: "flowchart-demo"
    });
    var promise = loadData();
    $.when(promise).then(function () {
        groupByName();
        renderGroups();
        connectControllers();
    })
    jsPlumb.fire("jsPlumbDemoLoaded", instance);
});
<|endoftext|>


<commit_before>/*jslint
    indent: 4,
    maxerr: 50,
    white: true,
    browser: true,
    vars: true
*/
/*global
    define,
    require,
    getComputedStyle
*/

/**
 * Show Settings Menu
 * @fileOverview Show Settings Menu

 * Displays an interactive settings menu mostly generated on the fly based on
 *  the current state of the editor.
 * @author
 *  Matthew Christopher Kastor-Inare III

 *  ☭ Hial Atropa!! ☭
 */

define(function(require, exports, module) {
    "use strict";
    var overlayPage = require('./overlay_page').overlayPage;
    var addEditorMenuOptions = require('./add_editor_menu_options').addEditorMenuOptions;

    /**
     * These functions are necessary for the settings menu
     * to provide a couple really useful settings.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {ace.Editor} editor An instance of the ace editor.
     */
    function addFunctionsForSettingsMenu (editor) {
        // when building the settings menu matching get and set functions
        // must be found or the function will be ignored

        editor.getFontSize = function () {
            return getComputedStyle(
                editor.container).getPropertyValue('font-size');
        };

        // this allows the settings menu to supply a wrap limit
        // using a text input field easily
        editor.session.setWrapLimit = function (limit) {
            editor.session.setWrapLimitRange(limit, limit);
        };
    }
    /**
     * Generates a list of set functions for the settings menu.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {object} editor The editor instance
     * @return {array} Returns an array of objects. Each object contains the
     *  following properties: functionName, parentObj, and parentName. The
     *  function name will be the name of a method beginning with the string
     *  `set` which was found. The parent object will be a reference to the
     *  object having the method matching the function name. The parent name
     *  will be a string representing the identifier of the parent object e.g.
     *  `editor`, `session`, or `renderer`.
     */
    function getSetFunctions (editor) {
        /**
         * Output array. Will hold the objects described above.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var out = [];
        /**
         * This object provides a map between the objects which will be
         *  traversed and the parent name which will appear in the output.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var my = {
            'editor' : editor,
            'session' : editor.session,
            'renderer' : editor.renderer
        };
        /**
         * This array will hold the set function names which have already been
         *  found so that they are not added to the output multiple times.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var opts = [];
        /**
         * This is a list of set functions which will not appear in the settings
         *  menu. I don't know what to do with setKeyboardHandler. When I tried
         *  to use it, it didn't appear to be working. Someone who knows better
         *  could remove it from this list and add it's options to
         *  add_editor_menu_options.js
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var skip = [
            'setOption',
            'setUndoManager',
            'setDocument',
            'setValue',
            'setBreakpoints',
            'setScrollTop',
            'setScrollLeft',
            'setSelectionStyle',
            'setWrapLimitRange',
            'setKeyboardHandler'
        ];

        /**
         * This will search the objects mapped to the `my` variable above. When
         *  it finds a set function in the object that is not listed in the
         *  `skip` list or the `opts` list it will push a new object to the
         *  output array.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        [
            'renderer',
            'session',
            'editor'
        ].forEach(function (esra) {
            var fn;
            var esr = my[esra];
            var clss = esra;
            for(fn in esr) {
                if(skip.indexOf(fn) === -1) {
                    if(/^set/.test(fn) && opts.indexOf(fn) === -1) {
                        // found set function
                        opts.push(fn);
                        out.push({
                            'functionName' : fn,
                            'parentObj' : esr,
                            'parentName' : clss
                        });
                    }
                }
            }
        });
        return out;
    }
    /**
     * Generates an interactive menu with settings useful to end users.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {ace.Editor} editor An instance of the ace editor.
     */
    function generateMenu (editor) {
        /**
         * container for dom elements that will go in the menu.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var elements = [];
        /**
         * Sorts the menu entries (elements var) so they'll appear in alphabetical order
         *  the sort is performed based on the value of the contains property
         *  of each element. Since this is an `array.sort` the array is sorted
         *  in place.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        function cleanupElementsList() {
            elements.sort(function (a, b) {
                var x = a.getAttribute('contains');
                var y = b.getAttribute('contains');
                return x.localeCompare(y);
            });
        }
        /**
         * Wraps all dom elements contained in the elements var with a single
         *  div.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        function wrapElements() {
            var topmenu = document.createElement('div');
            topmenu.id = 'settingsmenu';
            elements.forEach(function (element) {
                topmenu.appendChild(element);
            });
            return topmenu;
        }

        /**
         * Creates a new menu entry.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         * @param {object} obj This is a reference to the object containing the
         *  set function. It is used to set up event listeners for when the
         *  menu options change.
         * @param {string} clss Maps to the class of the dom element. This is
         *  the name of the object containing the set function e.g. `editor`,
         *  `session`, `renderer`.
         * @param {string} item  This is the set function name. It maps to the
         *  id of the dom element (check, select, input) and to the "contains"
         *  attribute of the div holding both the element and its label.
         * @param {mixed} val This is the value of the setting. It is mapped to
         *  the dom element's value, checked, or selected option accordingly.
         */
        function createNewEntry(obj, clss, item, val) {
            var egen = require('./element_generator');
            var el;
            var div = document.createElement('div');
            div.setAttribute('contains', item);
            div.setAttribute('class', 'menuEntry');

            div.appendChild(egen.createLabel(item, item));

            if(Array.isArray(val)) {
                el = egen.createSelection(item, val, clss);
                el.addEventListener('change', function (e) {
                    try{
                        editor.menuOptions[e.target.id].forEach(function (x) {
                            if(x.textContent !== e.target.textContent) {
                                delete x.selected;
                            }
                        });
                        // editor.session['setMode']('ace/mode/javascript')
                        obj[e.target.id](e.target.value);
                    } catch (err) {
                        throw new Error(err);
                    }
                });
            } else if(typeof val === 'boolean') {
                el = egen.createCheckbox(item, val, clss);
                el.addEventListener('change', function (e) {
                    try{
                        // renderer['setHighlightGutterLine'](true);
                        obj[e.target.id](!!e.target.checked);
                    } catch (err) {
                        throw new Error(err);
                    }
                });
            } else {
                // this aids in giving the ability to specify settings through
                // post and get requests.
                // /ace_editor.html?setMode=ace/mode/html&setOverwrite=true
                el = egen.createInput(item, val, clss);
                el.addEventListener('blur', function (e) {
                    try{
                        if(e.target.value === 'true') {
                            obj[e.target.id](true);
                        } else if(e.target.value === 'false') {
                            obj[e.target.id](false);
                        } else {
                            obj[e.target.id](e.target.value);
                        }
                    } catch (err) {
                        throw new Error(err);
                    }
                });
            }
            div.appendChild(el);
            return div;
        }
        /**
         * Generates selection fields for the menu and populates their options
         *  using information from `editor.menuOptions`
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         * @param {string} item The set function name.
         * @param {object} esr A reference to the object having the set function.
         * @param {string} clss The name of the object containing the set function.
         * @param {string} fn The matching get function's function name.
         * @returns {DOMElement} Returns a dom element containing a selection
         *  element populated with options. The option whose value matches that
         *  returned from `esr[fn]()` will be selected.
         */
        function makeDropdown(item, esr, clss, fn) {
            var val = editor.menuOptions[item];
            val = val.map(function (valuex) {
                if(valuex.value === esr[fn]()) {
                    valuex.selected = 'selected';
                } else if(valuex.value === esr.$modeId) {
                    // is mode
                    valuex.selected = 'selected';
                }
                return valuex;
            });
            return createNewEntry(esr, clss, item, val);
        }

        /**
         * Processes the set functions returned from `getSetFunctions`. First it
         *  checks for menu options defined in `editor.menuOptons`. If no
         *  options are specified then it checks whether there is a get function
         *  (replace set with get) for the setting. When either of those
         *  conditions are met it will attempt to create a new entry for the
         *  settings menu and push it into the elements array defined above.
         *  It can only do so for get functions which return
         *  strings, numbers, and booleans. A special case is written in for
         *  `getMode` where it looks at the returned objects `$id` property and
         *  forwards that through instead. Other special cases could be written
         *  in but that would get a bit ridiculous.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         * @param {object} setObj An item from the array returned by
         *  `getSetFunctions`.
         */
        function handleSet (setObj) {
            var item = setObj.functionName;
            var esr = setObj.parentObj;
            var clss = setObj.parentName;
            var val;
            var fn = item.replace(/^set/, 'get');
            if(editor.menuOptions[item] !== undefined) {
                // has options for select element
                elements.push(makeDropdown(item, esr, clss, fn));
            } else if(typeof esr[fn] === 'function') {
                // has get function
                try {
                    val = esr[fn]();
                    if(typeof val === 'object') {
                        // setMode takes a string, getMode returns an object
                        // the $id property of that object is the string
                        // which may be given to setMode...
                        val = val.$id;
                    }
                    // the rest of the get functions return strings,
                    // booleans, or numbers.
                    elements.push(
                        createNewEntry(esr, clss, item, val)
                    );
                } catch (e) {
                    // if there are errors it is because the element
                    // does not belong in the settings menu
                }
            }
        }

        // gather the set functions
        getSetFunctions(editor).forEach(function (setObj) {
            // populate the elements array with good stuff.
            handleSet(setObj);
        });
        // sort the menu entries in the elements list so people can find
        // the settings in alphabetical order.
        cleanupElementsList();
        // dump the entries from the elements list and wrap them up in a div
        // then put the div into the generic menu and show it.
        overlayPage(wrapElements(), '0', '0', '0', null);
    }
    /**
     * This builds the settings menu and selects
     *  all the options currently in effect.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {ace.Editor} editor An instance of the ace editor.
     */
    module.exports.showSettingsMenu = function showSettingsMenu (editor) {
        // make sure the menu isn't open already.
        if(!document.getElementById('settingsmenu')) {
            addFunctionsForSettingsMenu(editor);
            addEditorMenuOptions(editor);
            generateMenu(editor);
        }
    };
});<commit_msg>prettified settings menu<commit_after>/*jslint
    indent: 4,
    maxerr: 50,
    white: true,
    browser: true,
    vars: true
*/
/*global
    define,
    require,
    getComputedStyle
*/

/**
 * Show Settings Menu
 * @fileOverview Show Settings Menu

 * Displays an interactive settings menu mostly generated on the fly based on
 *  the current state of the editor.
 * @author
 *  Matthew Christopher Kastor-Inare III

 *  ☭ Hial Atropa!! ☭
 */

define(function(require, exports, module) {
    "use strict";
    var overlayPage = require('./overlay_page').overlayPage;
    var addEditorMenuOptions = require('./add_editor_menu_options').addEditorMenuOptions;

    /**
     * These functions are necessary for the settings menu
     * to provide a couple really useful settings.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {ace.Editor} editor An instance of the ace editor.
     */
    function addFunctionsForSettingsMenu (editor) {
        // when building the settings menu matching get and set functions
        // must be found or the function will be ignored

        editor.getFontSize = function () {
            return getComputedStyle(
                editor.container).getPropertyValue('font-size');
        };

        // this allows the settings menu to supply a wrap limit
        // using a text input field easily
        editor.session.setWrapLimit = function (limit) {
            editor.session.setWrapLimitRange(limit, limit);
        };
    }
    /**
     * Generates a list of set functions for the settings menu.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {object} editor The editor instance
     * @return {array} Returns an array of objects. Each object contains the
     *  following properties: functionName, parentObj, and parentName. The
     *  function name will be the name of a method beginning with the string
     *  `set` which was found. The parent object will be a reference to the
     *  object having the method matching the function name. The parent name
     *  will be a string representing the identifier of the parent object e.g.
     *  `editor`, `session`, or `renderer`.
     */
    function getSetFunctions (editor) {
        /**
         * Output array. Will hold the objects described above.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var out = [];
        /**
         * This object provides a map between the objects which will be
         *  traversed and the parent name which will appear in the output.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var my = {
            'editor' : editor,
            'session' : editor.session,
            'renderer' : editor.renderer
        };
        /**
         * This array will hold the set function names which have already been
         *  found so that they are not added to the output multiple times.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var opts = [];
        /**
         * This is a list of set functions which will not appear in the settings
         *  menu. I don't know what to do with setKeyboardHandler. When I tried
         *  to use it, it didn't appear to be working. Someone who knows better
         *  could remove it from this list and add it's options to
         *  add_editor_menu_options.js
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var skip = [
            'setOption',
            'setUndoManager',
            'setDocument',
            'setValue',
            'setBreakpoints',
            'setScrollTop',
            'setScrollLeft',
            'setSelectionStyle',
            'setWrapLimitRange',
            'setKeyboardHandler'
        ];

        /**
         * This will search the objects mapped to the `my` variable above. When
         *  it finds a set function in the object that is not listed in the
         *  `skip` list or the `opts` list it will push a new object to the
         *  output array.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        [
            'renderer',
            'session',
            'editor'
        ].forEach(function (esra) {
            var fn;
            var esr = my[esra];
            var clss = esra;
            for(fn in esr) {
                if(skip.indexOf(fn) === -1) {
                    if(/^set/.test(fn) && opts.indexOf(fn) === -1) {
                        // found set function
                        opts.push(fn);
                        out.push({
                            'functionName' : fn,
                            'parentObj' : esr,
                            'parentName' : clss
                        });
                    }
                }
            }
        });
        return out;
    }
    /**
     * Generates an interactive menu with settings useful to end users.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {ace.Editor} editor An instance of the ace editor.
     */
    function generateMenu (editor) {
        /**
         * container for dom elements that will go in the menu.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        var elements = [];
        /**
         * Sorts the menu entries (elements var) so they'll appear in alphabetical order
         *  the sort is performed based on the value of the contains property
         *  of each element. Since this is an `array.sort` the array is sorted
         *  in place.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        function cleanupElementsList() {
            elements.sort(function (a, b) {
                var x = a.getAttribute('contains');
                var y = b.getAttribute('contains');
                return x.localeCompare(y);
            });
        }
        /**
         * Wraps all dom elements contained in the elements var with a single
         *  div.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         */
        function wrapElements() {
            var topmenu = document.createElement('div');
            topmenu.setAttribute('id', 'settingsmenu');
            elements.forEach(function (element) {
                topmenu.appendChild(element);
            });
            return topmenu;
        }

        /**
         * Creates a new menu entry.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         * @param {object} obj This is a reference to the object containing the
         *  set function. It is used to set up event listeners for when the
         *  menu options change.
         * @param {string} clss Maps to the class of the dom element. This is
         *  the name of the object containing the set function e.g. `editor`,
         *  `session`, `renderer`.
         * @param {string} item  This is the set function name. It maps to the
         *  id of the dom element (check, select, input) and to the "contains"
         *  attribute of the div holding both the element and its label.
         * @param {mixed} val This is the value of the setting. It is mapped to
         *  the dom element's value, checked, or selected option accordingly.
         */
        function createNewEntry(obj, clss, item, val) {
            var egen = require('./element_generator');
            var el;
            var div = document.createElement('div');
            div.setAttribute('contains', item);
            div.setAttribute('class', 'menuEntry');
            div.setAttribute('style', 'clear: both;');

            div.appendChild(egen.createLabel(item, item));

            if(Array.isArray(val)) {
                el = egen.createSelection(item, val, clss);
                el.addEventListener('change', function (e) {
                    try{
                        editor.menuOptions[e.target.id].forEach(function (x) {
                            if(x.textContent !== e.target.textContent) {
                                delete x.selected;
                            }
                        });
                        // editor.session['setMode']('ace/mode/javascript')
                        obj[e.target.id](e.target.value);
                    } catch (err) {
                        throw new Error(err);
                    }
                });
            } else if(typeof val === 'boolean') {
                el = egen.createCheckbox(item, val, clss);
                el.addEventListener('change', function (e) {
                    try{
                        // renderer['setHighlightGutterLine'](true);
                        obj[e.target.id](!!e.target.checked);
                    } catch (err) {
                        throw new Error(err);
                    }
                });
            } else {
                // this aids in giving the ability to specify settings through
                // post and get requests.
                // /ace_editor.html?setMode=ace/mode/html&setOverwrite=true
                el = egen.createInput(item, val, clss);
                el.addEventListener('blur', function (e) {
                    try{
                        if(e.target.value === 'true') {
                            obj[e.target.id](true);
                        } else if(e.target.value === 'false') {
                            obj[e.target.id](false);
                        } else {
                            obj[e.target.id](e.target.value);
                        }
                    } catch (err) {
                        throw new Error(err);
                    }
                });
            }
            el.style.cssText = 'float:right;';
            div.appendChild(el);
            return div;
        }
        /**
         * Generates selection fields for the menu and populates their options
         *  using information from `editor.menuOptions`
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         * @param {string} item The set function name.
         * @param {object} esr A reference to the object having the set function.
         * @param {string} clss The name of the object containing the set function.
         * @param {string} fn The matching get function's function name.
         * @returns {DOMElement} Returns a dom element containing a selection
         *  element populated with options. The option whose value matches that
         *  returned from `esr[fn]()` will be selected.
         */
        function makeDropdown(item, esr, clss, fn) {
            var val = editor.menuOptions[item];
            val = val.map(function (valuex) {
                if(valuex.value === esr[fn]()) {
                    valuex.selected = 'selected';
                } else if(valuex.value === esr.$modeId) {
                    // is mode
                    valuex.selected = 'selected';
                }
                return valuex;
            });
            return createNewEntry(esr, clss, item, val);
        }

        /**
         * Processes the set functions returned from `getSetFunctions`. First it
         *  checks for menu options defined in `editor.menuOptons`. If no
         *  options are specified then it checks whether there is a get function
         *  (replace set with get) for the setting. When either of those
         *  conditions are met it will attempt to create a new entry for the
         *  settings menu and push it into the elements array defined above.
         *  It can only do so for get functions which return
         *  strings, numbers, and booleans. A special case is written in for
         *  `getMode` where it looks at the returned objects `$id` property and
         *  forwards that through instead. Other special cases could be written
         *  in but that would get a bit ridiculous.
         * @author
         *  Matthew Christopher Kastor-Inare III

         *  ☭ Hial Atropa!! ☭
         * @param {object} setObj An item from the array returned by
         *  `getSetFunctions`.
         */
        function handleSet (setObj) {
            var item = setObj.functionName;
            var esr = setObj.parentObj;
            var clss = setObj.parentName;
            var val;
            var fn = item.replace(/^set/, 'get');
            if(editor.menuOptions[item] !== undefined) {
                // has options for select element
                elements.push(makeDropdown(item, esr, clss, fn));
            } else if(typeof esr[fn] === 'function') {
                // has get function
                try {
                    val = esr[fn]();
                    if(typeof val === 'object') {
                        // setMode takes a string, getMode returns an object
                        // the $id property of that object is the string
                        // which may be given to setMode...
                        val = val.$id;
                    }
                    // the rest of the get functions return strings,
                    // booleans, or numbers.
                    elements.push(
                        createNewEntry(esr, clss, item, val)
                    );
                } catch (e) {
                    // if there are errors it is because the element
                    // does not belong in the settings menu
                }
            }
        }

        // gather the set functions
        getSetFunctions(editor).forEach(function (setObj) {
            // populate the elements array with good stuff.
            handleSet(setObj);
        });
        // sort the menu entries in the elements list so people can find
        // the settings in alphabetical order.
        cleanupElementsList();
        // dump the entries from the elements list and wrap them up in a div
        // then put the div into the generic menu and show it.
        overlayPage(wrapElements(), '0', '0', '0', '75%');
    }
    /**
     * This builds the settings menu and selects
     *  all the options currently in effect.
     * @author
     *  Matthew Christopher Kastor-Inare III

     *  ☭ Hial Atropa!! ☭
     * @param {ace.Editor} editor An instance of the ace editor.
     */
    module.exports.showSettingsMenu = function showSettingsMenu (editor) {
        // make sure the menu isn't open already.
        if(!document.getElementById('settingsmenu')) {
            addFunctionsForSettingsMenu(editor);
            addEditorMenuOptions(editor);
            generateMenu(editor);
        }
    };
});<|endoftext|>


<commit_before>'use strict';

const webdriver = require('selenium-webdriver');
const log = require('intel').getLogger('browsertime.command.addText');

class AddText {
  constructor(browser) {
    this.browser = browser;
  }

  /**
   * Add text to an element with Selenium sendKeys.
   * @param {string} text The text string that you want to add
   * @param {string} id The id of the element
   * @returns {Promise} Promise object represents when the text has been
   * added to the field
   * @throws Will throw an error if the element is not found
   */
  async byId(text, id) {
    const driver = this.browser.getDriver();
    try {
      const element = await driver.findElement(webdriver.By.id(id));
      // make sure we clear it
      await element.clear();
      return element.sendKeys(text);
    } catch (e) {
      log.error('Could not add text %s to id %s', text, id);
      log.verbose(e);
      throw e;
    }
  }

  /**
   * Add text to an element with Selenium sendKeys.
   * @param {string} text The text string that you want to add
   * @param {string} xpath The xpath to the element
   * @returns {Promise} Promise object represents when the text has been
   * added to the field
   * @throws Will throw an error if the element is not found
   */
  async byXpath(text, xpath) {
    const driver = this.browser.getDriver();
    try {
      const element = await driver.findElement(webdriver.By.xpath(xpath));
      // make sure we clear it
      await element.clear();
      return element.sendKeys(text);
    } catch (e) {
      log.error('Could not add text %s to xpath %s', text, xpath);
      log.verbose(e);
      throw e;
    }
  }
}
module.exports = AddText;
<commit_msg>add text by css selector (#764)<commit_after>'use strict';

const webdriver = require('selenium-webdriver');
const log = require('intel').getLogger('browsertime.command.addText');

class AddText {
  constructor(browser) {
    this.browser = browser;
  }

  /**
   * Add text to an element with Selenium sendKeys.
   * @param {string} text The text string that you want to add
   * @param {string} id The id of the element
   * @returns {Promise} Promise object represents when the text has been
   * added to the field
   * @throws Will throw an error if the element is not found
   */
  async byId(text, id) {
    const driver = this.browser.getDriver();
    try {
      const element = await driver.findElement(webdriver.By.id(id));
      // make sure we clear it
      await element.clear();
      return element.sendKeys(text);
    } catch (e) {
      log.error('Could not add text %s to id %s', text, id);
      log.verbose(e);
      throw e;
    }
  }

  /**
   * Add text to an element with Selenium sendKeys.
   * @param {string} text The text string that you want to add
   * @param {string} xpath The xpath to the element
   * @returns {Promise} Promise object represents when the text has been
   * added to the field
   * @throws Will throw an error if the element is not found
   */
  async byXpath(text, xpath) {
    const driver = this.browser.getDriver();
    try {
      const element = await driver.findElement(webdriver.By.xpath(xpath));
      // make sure we clear it
      await element.clear();
      return element.sendKeys(text);
    } catch (e) {
      log.error('Could not add text %s to xpath %s', text, xpath);
      log.verbose(e);
      throw e;
    }
  }

  /**
   * Add text to an element with Selenium sendKeys.
   * @param {string} text The text string that you want to add
   * @param {string} selector The CSS selector to the element
   * @returns {Promise} Promise object represents when the text has been
   * added to the field
   * @throws Will throw an error if the element is not found
   */
  async bySelector(text, selector) {
    const driver = this.browser.getDriver();
    try {
      const element = await driver.findElement(webdriver.By.css(selector));
      // make sure we clear it
      await element.clear();
      return element.sendKeys(text);
    } catch (e) {
      log.error('Could not add text %s to selector %s', text, selector);
      log.verbose(e);
      throw e;
    }
  }
}
module.exports = AddText;
<|endoftext|>


<commit_before>if (
  !window.Worker ||
  !window.Intl ||
  !window.Intl.DateTimeFormat ||
  !("onhashchange" in window) ||
  !("history" in window) ||
  !('content' in document.createElement('template')) ||
  !("classList" in document.createElement("_"))
) {
  document.body.className = "unsupported";
}

(function() {
  if (!window.data) { // let the data load
    var nodes = Array.prototype.slice.call(document.head.childNodes).filter(function(node) {
      return node.nodeType === Node.ELEMENT_NODE
        && node.nodeName === 'SCRIPT'
        && /data\.js$/.test(node.src);
    }).forEach(function(node) {
      node.addEventListener('load', build, false);
    });
  } else {
    build();
  }

  var local = document.body.querySelector('.time.local');
  var game = document.body.querySelector('.time.game');
  var formatter = new Intl.DateTimeFormat(undefined, {
    hour: 'numeric', minute: 'numeric', timeZone: 'UTC'
  });
  var sorts = {
    'default': function(a, b) {
      var delta = Number(a.dataset.time) - Number(b.dataset.time);
      if (delta !== 0) return delta;
      delta = Number(a.dataset.nodeid) - Number(b.dataset.nodeid);
      if (delta !== 0) return delta;
      return Number(a.dataset.slot) - Number(b.dataset.slot);

    }
  };
  try {
    // Start the web wokrer
    var worker = new Worker("worker.js");
    worker.onmessage = function(e) {
      local.textContent = e.data[1];
      game.textContent = e.data[2];
    };
  } catch(e) {
    console.error(e);
  }

  window.addEventListener("hashchange", function(e) {
    console.log(e);
  }, false);

  function resort(parent, elems, compare) {
    elems = Array.prototype.slice.call(elems || list.childNodes || [])
    .filter(function(node) {
      return node.nodeType === Node.ELEMENT_NODE;
    });
    elems.sort(compare).forEach(function(elem) {
      parent.appendChild(elem);
    });
  }

  function build() {
    mining(data.filter(function(item) {
      return item.type === 'mining';
    }));
    botany(data.filter(function(item) {
      return item.type === 'botany';
    }));
    fishing(data.filter(function(item) {
      return item.type === 'fishing';
    }));

    Array.prototype.slice.call(
      document.body.querySelectorAll('li[data-nodeid]')
    ).forEach(function(node, idx, arr) {
      node.addEventListener('click', function(e) {
        var target = e.currentTarget;
        arr.forEach(function(check) {
          if (check.dataset.nodeid === target.dataset.nodeid)
            check.classList.toggle('selected');
        });
        hash();
      });
    });

    unhash();
  }

  function mining(data) {
    var list = document.body.querySelector('section.mining ol');
    list.innerHTML = '';

    var content = document.body.querySelector('template.mining.row').content;
    resort(list, data.map(function(item, idx) {
      var li = content.querySelector('li');
      Object.keys(item).forEach(function(key) {
        li.dataset[key] = item[key];
      });
      li.dataset.id = idx;
      li.querySelector('.time').textContent = formatter.format(new Date(item.time * 1000));
      li.querySelector('.name').textContent = item.name;
      li.querySelector('.location').textContent = item.location;

      return document.importNode(content, true).firstElementChild;
    }), sorts.default);
  }

  function botany(data) {
    var list = document.body.querySelector('section.botany ol');
    list.innerHTML = '';

    // use the mining template for now.
    var content = document.body.querySelector('template.mining.row').content;
    resort(list, data.map(function(item, idx) {
      var li = content.querySelector('li');
      Object.keys(item).forEach(function(key) {
        li.dataset[key] = item[key];
      });
      li.dataset.id = idx;
      li.querySelector('.time').textContent = formatter.format(new Date(item.time * 1000));
      li.querySelector('.name').textContent = item.name;
      li.querySelector('.location').textContent = item.location;

      return document.importNode(content, true).firstElementChild;
    }), sorts.default);
  }

  function fishing(data) {
    var list = document.body.querySelector('section.fishing ol');
    list.innerHTML = '';

    // use the mining template for now.
    var content = document.body.querySelector('template.mining.row').content;
    resort(list, data.map(function(item, idx) {
      var li = content.querySelector('li');
      Object.keys(item).forEach(function(key) {
        li.dataset[key] = item[key];
      });
      li.dataset.id = idx;
      li.querySelector('.time').textContent = formatter.format(new Date(item.time * 1000));
      li.querySelector('.name').textContent = item.name;
      li.querySelector('.location').textContent = item.location;

      return document.importNode(content, true).firstElementChild;
    }), sorts.default);
  }

  /**
   * Construct a mask of selected nodeids and encode it into the location hash.
   */
  function hash() {
    var data = [];
    Array.prototype.slice.call(
      document.body.querySelectorAll('li[data-nodeid]')
    ).forEach(function(node) {
      if (node.classList.contains('selected')) {
        var id = Number(node.dataset.nodeid)
        var idx = ~~(id / 8);
        var val = id % 8;
        data[idx] = (data[idx] || 0) | (1 << val);
      }
    });
    for (var i = 0; i < data.length; i++)
      data[i] = String.fromCharCode(data[i] || 0);
    history.replaceState(undefined, undefined, '#' + btoa(data.join('')));
  }

  /**
   * Using the location hash as a nodeid mask, mark the nodes selected.
   */
  function unhash() {
    var hash = window.location.hash.substring(1);
    var nodeid = 0;
    var data = atob(hash);
    for (var idx = 0; idx < data.length; idx++) {
      var mask = data.charCodeAt(idx);
      for (var j = 8; j > 0; j--) {
        var id = nodeid++;
        if (mask && (mask & 1)) {
          var nodes = document.body.querySelectorAll('li[data-nodeid="' + id + '"]');
          Array.prototype.slice.call(nodes || []).forEach(function(node) {
            node.classList.add('selected')
          });
        }
        mask >>>= 1;
      }
    };
  }
})();
<commit_msg>bind unhash to the hash change event<commit_after>if (
  !window.Worker ||
  !window.Intl ||
  !window.Intl.DateTimeFormat ||
  !("onhashchange" in window) ||
  !("history" in window) ||
  !('content' in document.createElement('template')) ||
  !("classList" in document.createElement("_"))
) {
  document.body.className = "unsupported";
}

(function() {
  if (!window.data) { // let the data load
    var nodes = Array.prototype.slice.call(document.head.childNodes).filter(function(node) {
      return node.nodeType === Node.ELEMENT_NODE
        && node.nodeName === 'SCRIPT'
        && /data\.js$/.test(node.src);
    }).forEach(function(node) {
      node.addEventListener('load', build, false);
    });
  } else {
    build();
  }

  var local = document.body.querySelector('.time.local');
  var game = document.body.querySelector('.time.game');
  var formatter = new Intl.DateTimeFormat(undefined, {
    hour: 'numeric', minute: 'numeric', timeZone: 'UTC'
  });
  var sorts = {
    'default': function(a, b) {
      var delta = Number(a.dataset.time) - Number(b.dataset.time);
      if (delta !== 0) return delta;
      delta = Number(a.dataset.nodeid) - Number(b.dataset.nodeid);
      if (delta !== 0) return delta;
      return Number(a.dataset.slot) - Number(b.dataset.slot);

    }
  };
  try {
    // Start the web wokrer
    var worker = new Worker("worker.js");
    worker.onmessage = function(e) {
      local.textContent = e.data[1];
      game.textContent = e.data[2];
    };
  } catch(e) {
    console.error(e);
  }

  window.addEventListener("hashchange", function(e) {
    console.log(e);
  }, false);

  function resort(parent, elems, compare) {
    elems = Array.prototype.slice.call(elems || list.childNodes || [])
    .filter(function(node) {
      return node.nodeType === Node.ELEMENT_NODE;
    });
    elems.sort(compare).forEach(function(elem) {
      parent.appendChild(elem);
    });
  }

  function build() {
    mining(data.filter(function(item) {
      return item.type === 'mining';
    }));
    botany(data.filter(function(item) {
      return item.type === 'botany';
    }));
    fishing(data.filter(function(item) {
      return item.type === 'fishing';
    }));

    Array.prototype.slice.call(
      document.body.querySelectorAll('li[data-nodeid]')
    ).forEach(function(node, idx, arr) {
      node.addEventListener('click', function(e) {
        var target = e.currentTarget;
        arr.forEach(function(check) {
          if (check.dataset.nodeid === target.dataset.nodeid)
            check.classList.toggle('selected');
        });
        hash();
      });
    });

    unhash();
  }

  function mining(data) {
    var list = document.body.querySelector('section.mining ol');
    list.innerHTML = '';

    var content = document.body.querySelector('template.mining.row').content;
    resort(list, data.map(function(item, idx) {
      var li = content.querySelector('li');
      Object.keys(item).forEach(function(key) {
        li.dataset[key] = item[key];
      });
      li.dataset.id = idx;
      li.querySelector('.time').textContent = formatter.format(new Date(item.time * 1000));
      li.querySelector('.name').textContent = item.name;
      li.querySelector('.location').textContent = item.location;

      return document.importNode(content, true).firstElementChild;
    }), sorts.default);
  }

  function botany(data) {
    var list = document.body.querySelector('section.botany ol');
    list.innerHTML = '';

    // use the mining template for now.
    var content = document.body.querySelector('template.mining.row').content;
    resort(list, data.map(function(item, idx) {
      var li = content.querySelector('li');
      Object.keys(item).forEach(function(key) {
        li.dataset[key] = item[key];
      });
      li.dataset.id = idx;
      li.querySelector('.time').textContent = formatter.format(new Date(item.time * 1000));
      li.querySelector('.name').textContent = item.name;
      li.querySelector('.location').textContent = item.location;

      return document.importNode(content, true).firstElementChild;
    }), sorts.default);
  }

  function fishing(data) {
    var list = document.body.querySelector('section.fishing ol');
    list.innerHTML = '';

    // use the mining template for now.
    var content = document.body.querySelector('template.mining.row').content;
    resort(list, data.map(function(item, idx) {
      var li = content.querySelector('li');
      Object.keys(item).forEach(function(key) {
        li.dataset[key] = item[key];
      });
      li.dataset.id = idx;
      li.querySelector('.time').textContent = formatter.format(new Date(item.time * 1000));
      li.querySelector('.name').textContent = item.name;
      li.querySelector('.location').textContent = item.location;

      return document.importNode(content, true).firstElementChild;
    }), sorts.default);
  }

  /**
   * Construct a mask of selected nodeids and encode it into the location hash.
   */
  function hash() {
    var data = [];
    Array.prototype.slice.call(
      document.body.querySelectorAll('li[data-nodeid]')
    ).forEach(function(node) {
      if (node.classList.contains('selected')) {
        var id = Number(node.dataset.nodeid)
        var idx = ~~(id / 8);
        var val = id % 8;
        data[idx] = (data[idx] || 0) | (1 << val);
      }
    });
    for (var i = 0; i < data.length; i++)
      data[i] = String.fromCharCode(data[i] || 0);
    history.replaceState(undefined, undefined, '#' + btoa(data.join('')));
  }

  /**
   * Using the location hash as a nodeid mask, mark the nodes selected.
   */
  function unhash() {
    var hash = window.location.hash.substring(1);
    var nodeid = 0;
    var data = atob(hash);
    for (var idx = 0; idx < data.length; idx++) {
      var mask = data.charCodeAt(idx);
      for (var j = 8; j > 0; j--) {
        var id = nodeid++;
        if (mask && (mask & 1)) {
          var nodes = document.body.querySelectorAll('li[data-nodeid="' + id + '"]');
          Array.prototype.slice.call(nodes || []).forEach(function(node) {
            node.classList.add('selected')
          });
        }
        mask >>>= 1;
      }
    };
  }
  window.addEventListener("hashchange", unhash, false);
})();
<|endoftext|>