chromium/ash/webui/multidevice_debug/resources/logs.js

/* Copyright 2017 The Chromium Authors
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/**
 * @typedef {{
 *   text: string,
 *   time: string,
 *   file: string,
 *   line: number,
 *   severity: number,
 *}}
 */
let Log;

const Logs = {
  controller_: null,

  /**
   * Initializes the logs UI.
   */
  init: function() {
    Logs.controller_ = new LogsListController();

    const clearLogsButton = document.querySelector('#clear-logs-button');
    clearLogsButton.onclick = function() {
      WebUI.clearLogs();
    };

    const saveLogsButton = document.querySelector('#save-logs-button');
    saveLogsButton.onclick = () => {
      Logs.saveLogs();
    };

    WebUI.getLogMessages();
  },

  saveLogs: function() {
    const blob = new Blob(
        [document.querySelector('#logs-list').innerText],
        {type: 'text/plain;charset=utf-8'});
    const url = URL.createObjectURL(blob);

    const anchorEl = document.createElement('a');
    anchorEl.href = url;
    anchorEl.download = 'proximity_auth_logs_' + new Date().toJSON() + '.txt';
    document.body.appendChild(anchorEl);
    anchorEl.click();

    window.setTimeout(function() {
      document.body.removeChild(anchorEl);
      window.URL.revokeObjectURL(url);
    }, 0);
  },
};

/**
 * Interface with the native WebUI component for LogBuffer events. The functions
 * contained in this object will be invoked by the browser for each operation
 * performed on the native LogBuffer.
 */
const LogBufferInterface = {
  /**
   * Called when a new log message is added.
   * @param {!Log} log
   */
  onLogMessageAdded: function(log) {
    if (Logs.controller_) {
      Logs.controller_.add(log);
    }
  },

  /**
   * Called when the log buffer is cleared.
   */
  onLogBufferCleared: function() {
    if (Logs.controller_) {
      Logs.controller_.clear();
    }
  },

  /**
   * Called in response to chrome.send('getLogMessages') with the log messages
   * currently in the buffer.
   * @param {!Array<Log>} messages
   */
  onGotLogMessages: function(messages) {
    if (Logs.controller_) {
      Logs.controller_.set(messages);
    }
  },
};

/**
 * Controller for the logs list element, updating it based on user input and
 * logs received from native code.
 */
class LogsListController {
  constructor() {
    this.logsList_ = document.querySelector('#logs-list');
    this.itemTemplate_ = document.querySelector('#item-template');
    this.shouldSnapToBottom_ = true;

    this.logsList_.onscroll = this.onScroll_.bind(this);
  }

  /**
   * Listener for scroll event of the logs list element, used for snap to bottom
   * logic.
   */
  onScroll_() {
    const list = this.logsList_;
    this.shouldSnapToBottom_ =
        list.scrollTop + list.offsetHeight == list.scrollHeight;
  }

  /**
   * Clears all log items from the logs list.
   */
  clear() {
    const items = this.logsList_.querySelectorAll('.log-item');
    for (let i = 0; i < items.length; ++i) {
      items[i].remove();
    }
    this.shouldSnapToBottom_ = true;
  }

  /**
   * Adds a log to the logs list.
   * @param {!Log} log
   */
  add(log) {
    const directories = log.file.split('/');
    const source = directories[directories.length - 1] + ':' + log.line;

    const t = this.itemTemplate_.content;
    t.querySelector('.log-item').attributes.severity.value = log.severity;
    t.querySelector('.item-time').textContent = log.time;
    t.querySelector('.item-source').textContent = source;
    t.querySelector('.item-text').textContent = log.text;

    const newLogItem = document.importNode(this.itemTemplate_.content, true);
    this.logsList_.appendChild(newLogItem);
    if (this.shouldSnapToBottom_) {
      this.logsList_.scrollTop = this.logsList_.scrollHeight;
    }
  }

  /**
   * Initializes the log list from an array of logs.
   * @param {!Array<!Log>} logs
   */
  set(logs) {
    this.clear();
    for (let i = 0; i < logs.length; ++i) {
      this.add(logs[i]);
    }
  }
}