chromium/ash/webui/common/resources/traffic_counters/traffic_counters.ts

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

import '//resources/ash/common/network/network_shared.css.js';
import '//resources/ash/common/i18n_behavior.js';
import '//resources/ash/common/network/onc_mojo.js';

import {I18nMixin} from '//resources/ash/common/cr_elements/i18n_mixin.js';
import {TrafficCounter} from '//resources/mojo/chromeos/services/network_config/public/mojom/cros_network_config.mojom-webui.js';
import {NetworkType} from '//resources/mojo/chromeos/services/network_config/public/mojom/network_types.mojom-webui.js';
import {Time} from '//resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';
import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {OncMojo} from '../network/onc_mojo.js';

import {getTemplate} from './traffic_counters.html.js';
import {TrafficCountersAdapter} from './traffic_counters_adapter.js';

/**
 * @fileoverview Polymer element for a container used in displaying network
 * traffic information.
 */

/**
 * Image file name corresponding to network type.
 */
enum TechnologyIcons {
  CELLULAR = 'cellular_0.svg',
  ETHERNET = 'ethernet.svg',
  VPN = 'vpn.svg',
  WIFI = 'wifi_0.svg',
}

/**
 * Information about a network.
 */
interface Network {
  guid: string;
  name: string;
  type: NetworkType;
  counters: TrafficCounter[];
  lastResetTime: Time|null;
}

interface OnNetworkSelectedEvent {
  model: {network: Network};
}

/**
 * Helper function to create a Network object.
 */
function createNetwork(
  guid: string,
  name: string,
  type: NetworkType,
  counters: TrafficCounter[],
  lastResetTime: Time | null): Network {
  return {
    guid: guid,
    name: name,
    type: type,
    counters: counters,
    lastResetTime: lastResetTime,
  };
}

/**
 * Replacer function used to handle the bigint type.
 */
function replacer(_key: string, value: any): string|undefined|null {
  return typeof value === 'bigint' ? value.toString() : value;
}

/**
 * Converts a mojo time to JS. TODO(b/200327630)
 */
function convertMojoTimeToJS(mojoTime: Time): Date {
  // The JS Date() is based off of the number of milliseconds since the
  // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
  // base::Time (represented in mojom.Time) represents the number of
  // microseconds since the Windows FILETIME epoch (1601-01-01 00:00:00 UTC).
  // This computes the final JS time by computing the epoch delta and the
  // conversion from microseconds to milliseconds.
  const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
  const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
  // |epochDeltaInMs| equals to base::Time::kTimeToMicrosecondsOffset.
  const epochDeltaInMs = unixEpoch - windowsEpoch;
  const timeInMs = Number(mojoTime.internalValue) / 1000;

  return new Date(timeInMs - epochDeltaInMs);
}

export interface TrafficCountersElement {
  $: {
    container: HTMLDivElement,
  };
}

const TrafficCountersElementBase = I18nMixin(PolymerElement);

export class TrafficCountersElement extends TrafficCountersElementBase {
  static get is() {
    return 'traffic-counters' as const;
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /**
       * Information about networks.
       */
      networks_: {type: Array, value: []},

      /**
       * Expanded state per network type.
       */
      typeExpanded_: {type: Array, value: []},
    };
  }

  private networks_: Network[];
  private typeExpanded_: boolean[];
  private trafficCountersAdapter_: TrafficCountersAdapter;

  constructor() {
    super();

    /**
     * Adapter to access traffic counters functionality.
     */
    this.trafficCountersAdapter_ = new TrafficCountersAdapter();
  }

  /**
   * Handles requests to request traffic counters.
   */
  private async onRequestTrafficCountersClick_(): Promise<void> {
    await this.fetchTrafficCountersForActiveNetworks_();
  }

  /**
   * Handles requests to reset traffic counters.
   */
  private async onResetTrafficCountersClick_(event: OnNetworkSelectedEvent):
    Promise<void> {
    const network = event.model.network;
    await this.trafficCountersAdapter_.resetTrafficCountersForNetwork(
        network.guid);
    const trafficCounters =
        await this.trafficCountersAdapter_.requestTrafficCountersForNetwork(
            network.guid);
    const lastResetTime =
        await this.trafficCountersAdapter_.requestLastResetTimeForNetwork(
            network.guid);
    const foundIdx = this.networks_.findIndex(n => n.guid === network.guid);
    if (foundIdx === -1) {
      return;
    }
    this.splice(
        'networks_', foundIdx, 1,
        createNetwork(
            network.guid, network.name, network.type, trafficCounters,
            lastResetTime));
  }

  /**
   * Requests traffic counters for networks.
   */
  private async fetchTrafficCountersForActiveNetworks_(): Promise<Network[]> {
    const networks = await this.trafficCountersAdapter_
                         .requestTrafficCountersForActiveNetworks();
    this.networks_ = networks;
    return this.networks_;
  }

  private getNetworkTypeString_(type: NetworkType): string {
    return this.i18n('OncType' + OncMojo.getNetworkTypeString(type));
  }

  private getNetworkTypeIcon_(type: NetworkType): string {
    switch (type) {
      case NetworkType.kEthernet:
        return TechnologyIcons.ETHERNET;
      case NetworkType.kWiFi:
        return TechnologyIcons.WIFI;
      case NetworkType.kVPN:
        return TechnologyIcons.VPN;
      case NetworkType.kTether:
      case NetworkType.kMobile:
      case NetworkType.kCellular:
        return TechnologyIcons.CELLULAR;
      default:
        return '';
    }
  }

  private getTypeExpanded_(type: NetworkType): boolean {
    if (this.typeExpanded_[type] === undefined) {
      this.set('typeExpanded_.' + type, false);
      return false;
    }
    return this.typeExpanded_[type];
  }

  /**
   * Helper function to toggle the expanded properties when the network
   * container is toggled.
   */
  private onToggleExpanded_(event: OnNetworkSelectedEvent) {
    const type = event.model.network.type;
    this.set('typeExpanded_.' + type, !this.typeExpanded_[type]);
  }

  private countersToString_(counters: TrafficCounter[]): string {
    // '\t' describes the number of white space characters to use as white space
    // while forming the JSON string.
    return JSON.stringify(counters, replacer, '\t');
  }

  private lastResetTimeString_(network: Network): string {
    if (network.lastResetTime === null || network.lastResetTime === undefined) {
      return '';
    }
    return convertMojoTimeToJS(network.lastResetTime).toLocaleString();
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [TrafficCountersElement.is]: TrafficCountersElement;
  }
}

customElements.define(TrafficCountersElement.is, TrafficCountersElement);