chromium/chrome/browser/resources/components/components.ts

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

import './strings.m.js';

import {assert} from 'chrome://resources/js/assert.js';
import {addWebUiListener, sendWithPromise} from 'chrome://resources/js/cr.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {isChromeOS} from 'chrome://resources/js/platform.js';
import {$, getRequiredElement} from 'chrome://resources/js/util.js';

declare global {
  class JsEvalContext {
    constructor(data: any);
  }
  function jstProcess(context: JsEvalContext, template: HTMLElement): void;

  const trustedTypes: {emptyHTML: string};
}

interface Component {
  id: string;
  name: string;
  status: string;
  version: string;
}

interface ComponentsData {
  components: Component[];
  showOsLink: boolean;
}

/**
 * An array of the latest component data including ID, name, status and
 * version. This is populated in returnComponentsData() for the convenience of
 * tests.
 */
let currentComponentsData: Component[]|null = null;

/**
 * Takes the |componentsData| input argument which represents data about the
 * currently installed components and populates the html jstemplate with
 * that data. It expects an object structure like the above.
 * @param componentsData Detailed info about installed components.
 *      Same expected format as returnComponentsData().
 */
function renderTemplate(componentsData: ComponentsData) {
  // This is the javascript code that processes the template:
  const input = new JsEvalContext(componentsData);
  const output =
      document.body.querySelector<HTMLElement>(
                       '#component-template')!.cloneNode(true) as HTMLElement;
  getRequiredElement('component-placeholder').innerHTML =
      trustedTypes.emptyHTML;
  getRequiredElement('component-placeholder').appendChild(output);
  jstProcess(input, output);
  output.removeAttribute('hidden');

  // <if expr="is_chromeos">
  const crosUrlRedirectButton = $('os-link-href');
  if (crosUrlRedirectButton) {
    crosUrlRedirectButton.onclick = crosUrlComponentRedirect;
  }
  // </if>
}

// <if expr="is_chromeos">
/**
 * Called when the user clicks on the os-link-href button.
 */
function crosUrlComponentRedirect() {
  chrome.send('crosUrlComponentsRedirect');
}
// </if>

/**
 * Asks the C++ ComponentsDOMHandler to get details about the installed
 * components.
 */
function requestComponentsData() {
  sendWithPromise('requestComponentsData').then(returnComponentsData);
}

/**
 * Called by the WebUI to re-populate the page with data representing the
 * current state of installed components. The componentsData will also be
 * stored in currentComponentsData to be available to JS for testing purposes.
 * @param componentsData Detailed info about installed components.
 */
function returnComponentsData(componentsData: ComponentsData) {
  const bodyContainer = getRequiredElement('body-container');
  const body = document.body;

  bodyContainer.style.visibility = 'hidden';
  body.className = '';

  // Initialize |currentComponentsData|, which can also be updated in
  // onComponentEvent() later.
  currentComponentsData = componentsData.components;

  renderTemplate(componentsData);

  // Add handlers to dynamically created HTML elements.
  const links =
      document.body.querySelectorAll<HTMLButtonElement>('.button-check-update');
  for (const link of links) {
    link.onclick = function(e) {
      handleCheckUpdate(link);
      e.preventDefault();
    };
  }

  // Disable some controls for Guest mode in ChromeOS.
  if (isChromeOS && loadTimeData.getBoolean('isGuest')) {
    document.body.querySelectorAll<HTMLButtonElement>('[guest-disabled]')
        .forEach(function(element) {
          element.disabled = true;
        });
  }

  const systemFlagsLinkDiv = $('os-link-container');
  if (systemFlagsLinkDiv) {
    systemFlagsLinkDiv.hidden = !componentsData.showOsLink;
  }

  bodyContainer.style.visibility = 'visible';
  body.className = 'show-tmi-mode-initial';
}

interface ComponentEvent {
  event: string;
  id?: string;
  version?: string;
}

/**
 * Listener called when state of component updater service changes.
 * @param event Contains event and component ID. Component ID is
 *     optional.
 */
function onComponentEvent(event: ComponentEvent) {
  if (!event.id) {
    return;
  }

  const id = event.id;

  assert(currentComponentsData);
  const filteredComponents = currentComponentsData.filter(function(entry) {
    return entry.id === id;
  });

  // A component may be added from another page so the status and version
  // should only be updated if the component is listed on this page.
  if (filteredComponents.length === 0) {
    return;
  }

  const component = filteredComponents[0];
  assert(component);

  const status = event.event;
  getRequiredElement('status-' + id).textContent = status;
  component.status = status;

  if (event.version) {
    const version = event.version;
    getRequiredElement('version-' + id).textContent = version;
    component.version = version;
  }
}

/**
 * Handles an 'enable' or 'disable' button getting clicked.
 * @param node The HTML element representing the component being checked for
 *     update.
 */
function handleCheckUpdate(node: HTMLElement) {
  getRequiredElement('status-' + String(node.id)).textContent =
      loadTimeData.getString('checkingLabel');

  // Tell the C++ ComponentssDOMHandler to check for update.
  chrome.send('checkUpdate', [String(node.id)]);
}

// Get data and have it displayed upon loading.
document.addEventListener('DOMContentLoaded', function() {
  addWebUiListener('component-event', onComponentEvent);
  requestComponentsData();
});