chromium/components/sync/service/resources/sync_node_browser.ts

// Copyright 2011 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/cr_elements/cr_splitter/cr_splitter.js';
import 'chrome://resources/cr_elements/cr_tree/cr_tree.js';
import 'chrome://resources/cr_elements/cr_tree/cr_tree_item.js';

import type {CrTreeItemElement} from 'chrome://resources/cr_elements/cr_tree/cr_tree_item.js';
import {assert} from 'chrome://resources/js/assert.js';

import type {SyncNode, SyncNodeMap} from './chrome_sync.js';
import {getAllNodes} from './chrome_sync.js';

/**
 * A helper function to determine if a node is the root of its type.
 */
function isTypeRootNode(node: SyncNode): boolean {
  return node.PARENT_ID === 'r' && node.UNIQUE_SERVER_TAG !== '';
}

/**
 * A helper function to determine if a node is a child of the given parent.
 */
function isChildOf(parentNode: SyncNode, node: SyncNode) {
  if (node.PARENT_ID !== '') {
    return node.PARENT_ID === parentNode.ID;
  } else {
    return node.dataType === parentNode.dataType;
  }
}

/**
 * A helper function to sort sync nodes.
 *
 * Sorts by position index if possible, falls back to sorting by name, and
 * finally sorting by METAHANDLE.
 *
 * If this proves to be slow and expensive, we should experiment with moving
 * this functionality to C++ instead.
 */
function nodeComparator(nodeA: SyncNode, nodeB: SyncNode): number {
  if (nodeA.hasOwnProperty('positionIndex') &&
      nodeB.hasOwnProperty('positionIndex')) {
    return nodeA.positionIndex! - nodeB.positionIndex!;
  } else if (nodeA.NON_UNIQUE_NAME !== nodeB.NON_UNIQUE_NAME) {
    return nodeA.NON_UNIQUE_NAME.localeCompare(nodeB.NON_UNIQUE_NAME);
  } else {
    return nodeA.METAHANDLE - nodeB.METAHANDLE;
  }
}

/**
 * Updates the node detail view with the details for the given node.
 * @param node The struct representing the node we want to display.
 */
function updateNodeDetailView(node: CrTreeItemElement) {
  const nodeDetailsView = document.querySelector<HTMLElement>('#node-details');
  assert(nodeDetailsView);
  nodeDetailsView.hidden = false;
  const detail = node.detail as {payload: SyncNode};
  jstProcess(new JsEvalContext(detail.payload), nodeDetailsView);
}

/**
 * Updates the 'Last refresh time' display.
 * @param str The text to display.
 */
function setLastRefreshTime(str: string) {
  const refreshTime =
      document.querySelector<HTMLElement>('#node-browser-refresh-time');
  assert(refreshTime);
  refreshTime.textContent = str;
}

/**
 * Clears any existing UI state.  Useful prior to a refresh.
 */
function clear() {
  const treeContainer =
      document.querySelector<HTMLElement>('#sync-node-tree-container');
  assert(treeContainer);
  while (treeContainer.firstChild) {
    treeContainer.removeChild(treeContainer.firstChild);
  }

  const nodeDetailsView = document.querySelector<HTMLElement>('#node-details');
  assert(nodeDetailsView);
  nodeDetailsView.hidden = true;
}

function setNode(treeItem: CrTreeItemElement, node: SyncNode) {
  (treeItem.detail as {payload: SyncNode}).payload = node;
  treeItem.label = node.NON_UNIQUE_NAME;
  if (node.IS_DIR) {
    treeItem.toggleAttribute('may-have-children', true);

    // Load children on expand.
    treeItem.toggleAttribute('expanded', false);
    treeItem.addEventListener(
        'cr-tree-item-expand', () => handleExpand(treeItem));
  } else {
    treeItem.classList.add('leaf');
  }
}

function handleExpand(treeItem: CrTreeItemElement) {
  if (treeItem.hasChildren) {
    return;
  }

  const treeItemData = (treeItem.detail as {payload: SyncNode}).payload;
  const treeData = (treeItem.tree!.detail as {payload: SyncNode[]}).payload;
  const children = treeData.filter(node => isChildOf(treeItemData, node));
  children.sort(nodeComparator);

  children.forEach(function(node: SyncNode) {
    const item = document.createElement('cr-tree-item');
    treeItem.add(item);
    setNode(item, node);
  });
}

/**
 * Fetch the latest set of nodes and refresh the UI.
 */
function refresh() {
  const refreshButton =
      document.querySelector<HTMLButtonElement>('#node-browser-refresh-button');
  assert(refreshButton);
  refreshButton.disabled = true;

  clear();
  setLastRefreshTime('In progress since ' + (new Date()).toLocaleString());

  getAllNodes(function(nodeMap: SyncNodeMap) {
    let nodes: SyncNode[] = [];
    if (nodeMap && nodeMap.length > 0) {
      // Put all nodes into one big list that ignores the type.
      nodes = nodeMap.map(x => x.nodes).reduce((a, b) => a.concat(b));
    }

    const treeContainer =
        document.querySelector<HTMLElement>('#sync-node-tree-container');
    assert(treeContainer);
    const tree = document.createElement('cr-tree');
    tree.id = 'sync-node-tree';
    tree.addEventListener('cr-tree-change', () => {
      if (tree.selectedItem) {
        updateNodeDetailView(tree.selectedItem as CrTreeItemElement);
      }
    });
    treeContainer.appendChild(tree);

    tree.detail = {payload: nodes, children: {}};
    const roots = nodes.filter(isTypeRootNode);
    roots.sort(nodeComparator);
    roots.forEach(typeRoot => {
      const child = document.createElement('cr-tree-item');
      tree.add(child);
      setNode(child, typeRoot);
    });

    setLastRefreshTime((new Date()).toLocaleString());
    refreshButton.disabled = false;
  });
}

document.addEventListener('DOMContentLoaded', () => {
  const refreshButton =
      document.querySelector<HTMLButtonElement>('#node-browser-refresh-button');
  assert(refreshButton);
  refreshButton.addEventListener('click', refresh);
  const splitter = document.querySelector<HTMLElement>('#sync-node-splitter');
  assert(splitter);
  splitter.addEventListener('resize', () => {
    const treeElement =
        document.querySelector<HTMLElement>('#sync-node-tree-container');
    assert(treeElement);
    const newWidth = parseFloat(treeElement.style.width);
    treeElement.style.minWidth = Math.max(newWidth, 50) + 'px';
  });

  // Automatically trigger a refresh the first time this tab is selected.
  const tabBox = document.querySelector('cr-tab-box');
  assert(tabBox);
  tabBox.addEventListener('selected-index-change', function f() {
    const syncBrowserTab =
        document.querySelector<HTMLElement>('#sync-browser-tab');
    assert(syncBrowserTab);
    if (syncBrowserTab.hasAttribute('selected')) {
      assert(tabBox);
      tabBox.removeEventListener('selected-index-change', f);
      refresh();
    }
  });
});