chromium/ash/webui/diagnostics_ui/resources/network_card.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 './diagnostics_card.js';
import './diagnostics_network_icon.js';
import './diagnostics_shared.css.js';
import './ip_config_info_drawer.js';
import './network_info.js';
import './network_troubleshooting.js';

import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {PolymerElementProperties} from 'chrome://resources/polymer/v3_0/polymer/interfaces.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {TroubleshootingInfo} from './diagnostics_types.js';
import {filterNameServers, formatMacAddress, getNetworkCardTitle, getNetworkState, getNetworkType, isConnectedOrOnline, isNetworkMissingNameServers} from './diagnostics_utils.js';
import {getNetworkHealthProvider} from './mojo_interface_provider.js';
import {getTemplate} from './network_card.html.js';
import {Network, NetworkHealthProviderInterface, NetworkState, NetworkStateObserverReceiver, NetworkType} from './network_health_provider.mojom-webui.js';

const BASE_SUPPORT_URL = 'https://support.google.com/chromebook?p=diagnostics_';
const SETTINGS_URL = 'chrome://os-settings/';

/**
 * Represents the state of the network troubleshooting banner.
 */
export enum TroubleshootingState {
  DISABLED,
  NOT_CONNECTED,
  MISSING_IP_ADDRESS,
  MISSING_NAME_SERVERS,
}

/**
 * @fileoverview
 * 'network-card' is a styling wrapper for a network-info element.
 */

const NetworkCardElementBase = I18nMixin(PolymerElement);

export class NetworkCardElement extends NetworkCardElementBase {
  static get is(): 'network-card' {
    return 'network-card' as const;
  }

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

  static get properties(): PolymerElementProperties {
    return {
      guid: {
        type: String,
        value: '',
      },

      networkType: {
        type: String,
        value: '',
      },

      networkState: {
        type: String,
        value: '',
      },

      network: {
        type: Object,
      },

      showNetworkDataPoints: {
        type: Boolean,
        computed: 'computeShouldShowNetworkDataPoints(network.state,' +
            ' unableToObtainIpAddress, isMissingNameServers)',
      },

      showTroubleshootingCard: {
        type: Boolean,
        value: false,
      },

      macAddress: {
        type: String,
        value: '',
      },

      unableToObtainIpAddress: {
        type: Boolean,
        value: false,
      },

      troubleshootingInfo: {
        type: Object,
        computed: 'computeTroubleshootingInfo(network.*,' +
            ' unableToObtainIpAddress, isMissingNameServers)',
      },

      timerId: {
        type: Number,
        value: -1,
      },

      timeoutInMs: {
        type: Number,
        value: 30000,
      },

      isMissingNameServers: {
        type: Boolean,
        value: false,
      },

    };
  }

  guid: string;
  network: Network;
  protected showNetworkDataPoints: boolean;
  protected showTroubleshootingCard: boolean;
  protected macAddress: string;
  protected unableToObtainIpAddress: boolean;
  protected troubleshootingInfo: TroubleshootingInfo;
  protected isMissingNameServers: boolean;
  private networkType: string;
  private networkState: string;
  private timerId: number;
  private timeoutInMs: number;
  private networkHealthProvider: NetworkHealthProviderInterface =
      getNetworkHealthProvider();
  private networkStateObserverReceiver: NetworkStateObserverReceiver|null =
      null;

  static get observers(): string[] {
    return ['observeNetwork(guid)'];
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback();

    this.resetTimer();
  }

  private observeNetwork(): void {
    // If necessary, clear setTimeout and reset the timerId.
    this.resetTimer();

    // Reset this flag in case we were unable to obtain an IP Address for the
    // previous network.
    this.unableToObtainIpAddress = false;

    if (!this.guid) {
      return;
    }

    if (this.networkStateObserverReceiver) {
      this.networkStateObserverReceiver.$.close();
      this.networkStateObserverReceiver = null;
    }

    this.networkStateObserverReceiver = new NetworkStateObserverReceiver(this);

    this.networkHealthProvider.observeNetwork(
        this.networkStateObserverReceiver.$.bindNewPipeAndPassRemote(),
        this.guid);
  }

  /**
   * Implements NetworkStateObserver.onNetworkStateChanged
   */
  onNetworkStateChanged(network: Network): void {
    this.networkType = getNetworkType(network.type);
    this.networkState = getNetworkState(network.state);
    this.macAddress = network.macAddress || '';

    // Remove '0.0.0.0' (if present) from list of name servers.
    filterNameServers(network);
    this.set('network', network);
    const isIpAddressMissing = !network.ipConfig || !network.ipConfig.ipAddress;
    const isTimerInProgress = this.timerId !== -1;
    const isConnecting = network.state === NetworkState.kConnecting;

    if (!isIpAddressMissing) {
      this.isMissingNameServers = isNetworkMissingNameServers(network);
      // Reset this flag if the current network now has a valid IP Address.
      this.unableToObtainIpAddress = false;
    }

    if ((isIpAddressMissing && isConnecting) && !isTimerInProgress) {
      // Wait 30 seconds before displaying the troubleshooting banner.
      this.timerId = setTimeout(() => {
        this.resetTimer();
        this.unableToObtainIpAddress = true;
      }, this.timeoutInMs);
    }
  }

  protected getNetworkCardTitle(): string {
    return getNetworkCardTitle(this.networkType, this.networkState);
  }

  protected computeShouldShowNetworkDataPoints(): boolean {
    // Wait until the network is present before deciding.
    if (!this.network) {
      return false;
    }

    if (this.unableToObtainIpAddress || this.isMissingNameServers) {
      return false;
    }

    // Show the data-points when portal, online, connected, or connecting.
    switch (this.network.state) {
      case NetworkState.kPortal:
      case NetworkState.kOnline:
      case NetworkState.kConnected:
      case NetworkState.kConnecting:
        return true;
      default:
        return false;
    }
  }

  protected isNetworkDisabled(): boolean {
    return this.network.state === NetworkState.kDisabled;
  }

  protected getMacAddress(): string {
    if (!this.macAddress) {
      return '';
    }
    return formatMacAddress(this.macAddress);
  }

  private getDisabledTroubleshootingInfo(): TroubleshootingInfo {
    const linkText =
        this.network && this.network.type === NetworkType.kCellular ?
        this.i18n('reconnectLinkText') :
        this.i18n('joinNetworkLinkText', this.networkType);
    return {
      header: this.i18n('disabledText', this.networkType),
      linkText,
      url: SETTINGS_URL,
    };
  }

  private getNotConnectedTroubleshootingInfo(): TroubleshootingInfo {
    return {
      header: this.i18n('troubleshootingText', this.networkType),
      linkText: this.i18n('troubleConnecting'),
      url: BASE_SUPPORT_URL,
    };
  }

  private computeTroubleshootingInfo(): TroubleshootingInfo {
    let troubleshootingState: TroubleshootingState|null = null;
    if (!this.network || isConnectedOrOnline(this.network.state)) {
      // Hide the troubleshooting banner if we're in an active state
      // unlesss the network's IP Address has been missing for >= 30
      // seconds or we're missing name servers in which case we'd like
      // to display the bannner to the user.
      if (this.unableToObtainIpAddress) {
        troubleshootingState = TroubleshootingState.MISSING_IP_ADDRESS;
      }

      if (this.isMissingNameServers) {
        troubleshootingState = TroubleshootingState.MISSING_NAME_SERVERS;
      }

      if (troubleshootingState == null) {
        this.showTroubleshootingCard = false;
        return {
          header: '',
          linkText: '',
          url: '',
        };
      }
    }

    const isDisabled = this.network.state === NetworkState.kDisabled;
    // Show the not connected state for the Not Connected/Portal states.
    const isNotConnected = [
      NetworkState.kNotConnected,
      NetworkState.kPortal,
    ].includes(this.network.state);
    // Override the |troubleshootingState| value if necessary since the
    // disabled and not connected states take precedence.
    if (isNotConnected) {
      troubleshootingState = TroubleshootingState.NOT_CONNECTED;
    }

    if (isDisabled) {
      troubleshootingState = TroubleshootingState.DISABLED;
    }

    // At this point, |isConnectedOrOnline| is false, which means
    // out network state is either disabled or not connected.
    this.showTroubleshootingCard = true;

    return this.getInfoProperties(troubleshootingState);
  }

  private getMissingIpAddressInfo(): TroubleshootingInfo {
    return {
      header: this.i18n('noIpAddressText'),
      linkText: this.i18n('visitSettingsToConfigureLinkText'),
      url: SETTINGS_URL,
    };
  }

  private getMissingNameServersInfo(): TroubleshootingInfo {
    return {
      header: this.i18n('missingNameServersText'),
      linkText: this.i18n('visitSettingsToConfigureLinkText'),
      url: SETTINGS_URL,
    };
  }

  private getInfoProperties(state: TroubleshootingState|
                            null): TroubleshootingInfo {
    switch (state) {
      case TroubleshootingState.DISABLED:
        return this.getDisabledTroubleshootingInfo();
      case TroubleshootingState.NOT_CONNECTED:
        return this.getNotConnectedTroubleshootingInfo();
      case TroubleshootingState.MISSING_IP_ADDRESS:
        return this.getMissingIpAddressInfo();
      case TroubleshootingState.MISSING_NAME_SERVERS:
        return this.getMissingNameServersInfo();
      default:
        return {
          header: '',
          linkText: '',
          url: '',
        };
    }
  }

  private resetTimer(): void {
    if (this.timerId !== -1) {
      clearTimeout(this.timerId);
      this.timerId = -1;
    }
  }

  getTimerIdForTesting(): number {
    return this.timerId;
  }

  setTimeoutInMsForTesting(timeout: number): void {
    this.timeoutInMs = timeout;
  }

  getTimeoutInMsForTesting(): number {
    return this.timeoutInMs;
  }

  getUnableToObtainIpAddressForTesting(): boolean {
    return this.unableToObtainIpAddress;
  }
}

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

customElements.define(NetworkCardElement.is, NetworkCardElement);