blob: e15f3e7276917a809b92bab981ddeca5b9617f25 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/js/jstemplate_compiled.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {addWebUiListener, removeWebUiListener, WebUiListener} from 'chrome://resources/js/cr.js';
import {requestDataAndRegisterForUpdates, requestStart, requestStopClearData, setIncludeSpecifics, triggerRefresh} from './chrome_sync.js';
import {ProtocolEvent} from './traffic_log.js';
// Contains the latest snapshot of sync about info.
interface TypeStatus {
name: string;
num_entries: number;
num_live: number;
}
interface Detail {
is_sensitive: boolean;
}
export let aboutInfo: {details?: Detail[], type_status?: TypeStatus[]} = {};
// For tests
function getAboutInfoForTest() {
return aboutInfo;
}
let aboutInfoListener: WebUiListener|null = null;
let entityCountsUpdatedListener: WebUiListener|null = null;
function highlightIfChanged(node: HTMLElement, oldVal: number, newVal: number) {
const oldStr = oldVal.toString();
const newStr = newVal.toString();
if (oldStr !== '' && oldStr !== newStr) {
// Note the addListener function does not end up creating duplicate
// listeners. There can be only one listener per event at a time.
// Reference: https://developer.mozilla.org/en/DOM/element.addEventListener
node.addEventListener('webkitAnimationEnd', function() {
node.removeAttribute('highlighted');
}, false);
node.setAttribute('highlighted', '');
}
}
function refreshAboutInfo(newAboutInfo: object) {
aboutInfo = newAboutInfo;
const aboutInfoDiv = document.querySelector<HTMLElement>('#about-info');
assert(aboutInfoDiv);
jstProcess(new JsEvalContext(aboutInfo), aboutInfoDiv);
}
interface EntityCount {
modelType: string;
entities: number;
nonTombstoneEntities: number;
}
function onEntityCountsUpdatedEvent(response: {entityCounts: EntityCount[]}) {
if (!aboutInfo.type_status) {
return;
}
for (const count of response.entityCounts) {
const typeStatusRow =
aboutInfo.type_status.find(row => row.name === count.modelType);
if (typeStatusRow) {
typeStatusRow.num_entries = count.entities;
typeStatusRow.num_live = count.nonTombstoneEntities;
}
}
const typeInfo = document.querySelector<HTMLElement>('#typeInfo');
assert(typeInfo);
jstProcess(new JsEvalContext({type_status: aboutInfo.type_status}), typeInfo);
}
/**
* Helper to determine if an element is scrolled to its bottom limit.
* @param elem element to check
* @return true if the element is scrolled to the bottom
*/
function isScrolledToBottom(elem: HTMLElement): boolean {
return elem.scrollHeight - elem.scrollTop === elem.clientHeight;
}
/**
* Helper to scroll an element to its bottom limit.
*/
function scrollToBottom(elem: HTMLElement) {
elem.scrollTop = elem.scrollHeight - elem.clientHeight;
}
/** Container for accumulated sync protocol events. */
const protocolEvents: ProtocolEvent[] = [];
/** We may receive re-delivered events. Keep a record of ones we've seen. */
const knownEventTimestamps: {[key: string]: boolean} = {};
/**
* Callback for incoming protocol events.
* @param response The protocol event response.
*/
function onReceivedProtocolEvent(response: ProtocolEvent) {
// Return early if we've seen this event before. Assumes that timestamps
// are sufficiently high resolution to uniquely identify an event.
if (knownEventTimestamps.hasOwnProperty(response.time)) {
return;
}
knownEventTimestamps[response.time] = true;
protocolEvents.push(response);
const trafficContainer =
document.querySelector<HTMLElement>('#traffic-event-container');
assert(trafficContainer);
// Scroll to the bottom if we were already at the bottom. Otherwise, leave
// the scrollbar alone.
const shouldScrollDown = isScrolledToBottom(trafficContainer);
const context = new JsEvalContext({events: protocolEvents});
jstProcess(context, trafficContainer);
if (shouldScrollDown) {
scrollToBottom(trafficContainer);
}
}
/**
* Initializes state and callbacks for the protocol event log UI.
*/
function initProtocolEventLog() {
const includeSpecificsCheckbox =
document.querySelector<HTMLInputElement>('#capture-specifics');
assert(includeSpecificsCheckbox);
includeSpecificsCheckbox.addEventListener('change', () => {
setIncludeSpecifics(includeSpecificsCheckbox.checked);
});
addWebUiListener('onProtocolEvent', onReceivedProtocolEvent);
// Make the prototype jscontent element disappear.
const container =
document.querySelector<HTMLElement>('#traffic-event-container');
assert(container);
jstProcess({}, container);
const triggerRefreshButton =
document.querySelector<HTMLElement>('#trigger-refresh');
assert(triggerRefreshButton);
triggerRefreshButton.addEventListener('click', () => {
triggerRefresh();
});
}
/**
* Initializes listeners for status dump and import UI.
*/
function initStatusDumpButton() {
const statusData = document.querySelector<HTMLElement>('#status-data');
assert(statusData);
statusData.hidden = true;
const dumpStatusButton = document.querySelector<HTMLElement>('#dump-status');
assert(dumpStatusButton);
dumpStatusButton.addEventListener('click', () => {
const aboutInfoCopy = aboutInfo;
const includeIds = document.querySelector<HTMLInputElement>('#include-ids');
assert(includeIds);
if (!includeIds.checked) {
aboutInfoCopy.details = aboutInfo.details!.filter(function(el) {
return !el.is_sensitive;
});
}
let data = '';
data += new Date().toString() + '\n';
data += '======\n';
data += 'Status\n';
data += '======\n';
data += JSON.stringify(aboutInfoCopy, null, 2) + '\n';
const statusText =
document.querySelector<HTMLTextAreaElement>('#status-text');
assert(statusText);
statusText.value = data;
const statusData = document.querySelector<HTMLElement>('#status-data');
assert(statusData);
statusData.hidden = false;
});
const importStatusButton =
document.querySelector<HTMLElement>('#import-status');
assert(importStatusButton);
importStatusButton.addEventListener('click', () => {
const statusData = document.querySelector<HTMLElement>('#status-data');
assert(statusData);
statusData.hidden = false;
const statusText =
document.querySelector<HTMLTextAreaElement>('#status-text');
assert(statusText);
if (statusText.value.length === 0) {
statusText.value = 'Paste sync status dump here then click import.';
return;
}
// First remove any characters before the '{'.
let data = statusText.value;
const firstBrace = data.indexOf('{');
if (firstBrace < 0) {
statusText.value = 'Invalid sync status dump.';
return;
}
data = data.substr(firstBrace);
// Remove listeners to prevent sync events from overwriting imported data.
if (aboutInfoListener) {
removeWebUiListener(aboutInfoListener);
aboutInfoListener = null;
}
if (entityCountsUpdatedListener) {
removeWebUiListener(entityCountsUpdatedListener);
entityCountsUpdatedListener = null;
}
const aboutInfo = JSON.parse(data);
refreshAboutInfo(aboutInfo);
});
}
/**
* Toggles the given traffic event entry div's "expanded" state.
* @param e the click event that triggered the toggle.
*/
function expandListener(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains('proto')) {
// We ignore proto clicks to keep it copyable.
return;
}
let trafficEventDiv = e.target as HTMLElement;
// Click might be on div's child.
if (trafficEventDiv.nodeName !== 'DIV' && trafficEventDiv.parentNode) {
trafficEventDiv = trafficEventDiv.parentNode as HTMLElement;
}
trafficEventDiv.classList.toggle('traffic-event-entry-expanded');
}
/**
* Attaches a listener to the given traffic event entry div.
* @param element the element to attach the listener to.
*/
function addAboutExpandListener(element: HTMLElement) {
element.addEventListener('click', expandListener, false);
}
function onLoad() {
initStatusDumpButton();
initProtocolEventLog();
aboutInfoListener = addWebUiListener('onAboutInfoUpdated', refreshAboutInfo);
entityCountsUpdatedListener =
addWebUiListener('onEntityCountsUpdated', onEntityCountsUpdatedEvent);
const requestStartEl = document.querySelector<HTMLElement>('#request-start');
assert(requestStartEl);
requestStartEl.addEventListener('click', requestStart);
const requestStopClearDataEl =
document.querySelector<HTMLElement>('#request-stop-clear-data');
assert(requestStopClearDataEl);
requestStopClearDataEl.addEventListener('click', requestStopClearData);
// Request initial data for the page and listen to updates.
requestDataAndRegisterForUpdates();
}
// For JS eval and tests.
Object.assign(
window, {addAboutExpandListener, getAboutInfoForTest, highlightIfChanged});
document.addEventListener('DOMContentLoaded', onLoad, false);