chromium/components/policy/resources/webui/policy_row.ts

// Copyright 2023 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/action_link.js';
import './policy_conflict.js';
import './strings.m.js';

import {CustomElement} from 'chrome://resources/js/custom_element.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';

import type {Conflict, PolicyConflictElement} from './policy_conflict.js';
import {getTemplate} from './policy_row.html.js';

export interface Policy {
  ignored?: boolean;
  name: string;
  level: string;
  link?: string;
  scope: string;
  source: string;
  error: string;
  warning: string;
  info: string;
  value: any;
  deprecated?: boolean;
  future?: boolean;
  allSourcesMerged?: boolean;
  conflicts?: Conflict[];
  superseded?: Conflict[];
  forSigninScreen: boolean;
  isExtension: boolean;
  status: string;
}


export class PolicyRowElement extends CustomElement {
  static override get template() {
    return getTemplate();
  }

  policy: Policy;
  private unset_: boolean;
  private hasErrors_: boolean;
  private hasWarnings_: boolean;
  private hasInfos_: boolean;
  private hasConflicts_: boolean;
  private hasSuperseded_: boolean;
  private isMergedValue_: boolean;
  private deprecated_: boolean;
  private future_: boolean;

  connectedCallback() {
    const toggle = this.shadowRoot!.querySelector('.policy.row .toggle');
    toggle!.addEventListener('click', () => this.toggleExpanded());

    const copy = this.shadowRoot!.querySelector('.copy-value');
    copy!.addEventListener('click', () => this.copyValue_());

    this.setAttribute('role', 'rowgroup');
    this.classList.add('policy-data');
  }

  initialize(policy: Policy) {
    this.policy = policy;

    this.unset_ = policy.value === undefined;

    this.hasErrors_ = !!policy.error;

    this.hasWarnings_ = !!policy.warning;

    this.hasInfos_ = !!policy.info;

    this.hasConflicts_ = !!policy.conflicts;

    this.hasSuperseded_ = !!policy.superseded;

    this.isMergedValue_ = !!policy.allSourcesMerged;

    this.deprecated_ = !!policy.deprecated;

    this.future_ = !!policy.future;

    // Populate the name column.
    const nameDisplay = this.shadowRoot!.querySelector('.name .link span');
    nameDisplay!.textContent = policy.name;
    if (policy.link) {
      const link = this.getRequiredElement<HTMLAnchorElement>('.name .link');
      link.href = policy.link;
      link.title = loadTimeData.getStringF('policyLearnMore', policy.name);
      this.toggleAttribute('no-help-link', false);
    } else {
      this.toggleAttribute('no-help-link', true);
    }

    // Populate the remaining columns with policy scope, level and value if a
    // value has been set. Otherwise, leave them blank.
    if (!this.unset_) {
      const scopeDisplay = this.shadowRoot!.querySelector('.scope');
      let scope = 'scopeDevice';
      if (policy.scope === 'user') {
        scope = 'scopeUser';
      } else if (policy.scope === 'allUsers') {
        scope = 'scopeAllUsers';
      }
      scopeDisplay!.textContent = loadTimeData.getString(scope);

      // Display scope and level as rows instead of columns on space constraint
      // devices.
      // <if expr="is_android or is_ios">
      const scopeRowContentDisplay =
          this.shadowRoot!.querySelector('.scope.row .value');
      scopeRowContentDisplay!.textContent = loadTimeData.getString(scope);
      const levelRowContentDisplay =
          this.shadowRoot!.querySelector('.level.row .value');
      levelRowContentDisplay!.textContent = loadTimeData.getString(
          policy.level === 'recommended' ? 'levelRecommended' :
                                           'levelMandatory');
      // </if>

      const levelDisplay = this.shadowRoot!.querySelector('.level');
      levelDisplay!.textContent = loadTimeData.getString(
          policy.level === 'recommended' ? 'levelRecommended' :
                                           'levelMandatory');

      const sourceDisplay = this.shadowRoot!.querySelector('.source');
      sourceDisplay!.textContent = loadTimeData.getString(policy.source);
      // Reduces load on the DOM for long values;

      const convertValue = (value: string, format?: boolean) => {
        // Skip 'string' policy to avoid unnecessary conversions.
        if (typeof value == 'string') {
          return value;
        }
        if (format) {
          return JSON.stringify(value, null, 2);
        } else {
          return JSON.stringify(value, null);
        }
      };

      // If value is longer than 256 characters, truncate and add ellipsis.
      const policyValueStr = convertValue(policy.value);
      const truncatedValue = policyValueStr.length > 256 ?
          `${policyValueStr.substring(0, 256)}\u2026` :
          policyValueStr;

      const valueDisplay = this.shadowRoot!.querySelector('.value');
      valueDisplay!.textContent = truncatedValue;

      const copyLink = this.getRequiredElement('.copy .link');
      copyLink.title = loadTimeData.getStringF('policyCopyValue', policy.name);

      const valueRowContentDisplay =
          this.shadowRoot!.querySelector('.value.row .value');
      // Expanded policy value is formatted.
      valueRowContentDisplay!.textContent =
          convertValue(policy.value, /*format=*/ true);

      const errorRowContentDisplay =
          this.shadowRoot!.querySelector('.errors.row .value');
      errorRowContentDisplay!.textContent = policy.error;
      const warningRowContentDisplay =
          this.shadowRoot!.querySelector('.warnings.row .value');
      warningRowContentDisplay!.textContent = policy.warning;
      const infoRowContentDisplay =
          this.shadowRoot!.querySelector('.infos.row .value');
      infoRowContentDisplay!.textContent = policy.info;

      const messagesDisplay = this.shadowRoot!.querySelector('.messages');
      const errorsNotice =
          this.hasErrors_ ? loadTimeData.getString('error') : '';
      const deprecationNotice =
          this.deprecated_ ? loadTimeData.getString('deprecated') : '';
      const futureNotice = this.future_ ? loadTimeData.getString('future') : '';
      const warningsNotice =
          this.hasWarnings_ ? loadTimeData.getString('warning') : '';
      const conflictsNotice = this.hasConflicts_ && !this.isMergedValue_ ?
          loadTimeData.getString('conflict') :
          '';
      const ignoredNotice =
          this.policy.ignored ? loadTimeData.getString('ignored') : '';
      let notice =
          [
            errorsNotice,
            deprecationNotice,
            futureNotice,
            warningsNotice,
            ignoredNotice,
            conflictsNotice,
          ].filter(x => !!x)
              .join(', ') ||
          loadTimeData.getString('ok');
      const supersededNotice = this.hasSuperseded_ && !this.isMergedValue_ ?
          loadTimeData.getString('superseding') :
          '';
      if (supersededNotice) {
        // Include superseded notice regardless of other notices
        notice += `, ${supersededNotice}`;
      }
      messagesDisplay!.textContent = notice;
      policy.status = notice;

      if (policy.conflicts) {
        policy.conflicts.forEach(conflict => {
          const row = document.createElement('policy-conflict') as
              PolicyConflictElement;
          row.initialize(conflict, 'conflictValue');
          row.classList!.add('policy-conflict-data');
          this.shadowRoot!.appendChild(row);
        });
      }
      if (policy.superseded) {
        policy.superseded.forEach(superseded => {
          const row = document.createElement('policy-conflict') as
              PolicyConflictElement;
          row.initialize(superseded, 'supersededValue');
          row.classList!.add('policy-superseded-data');
          this.shadowRoot!.appendChild(row);
        });
      }
    } else {
      const messagesDisplay = this.shadowRoot!.querySelector('.messages');
      messagesDisplay!.textContent = loadTimeData.getString('unset');
    }
  }

  // Copies the policy's value to the clipboard.
  private copyValue_() {
    const policyValueDisplay =
        this.shadowRoot!.querySelector('.value.row .value');

    // Select the text that will be copied.
    const selection = window.getSelection();
    const range = window.document.createRange();
    range.selectNodeContents(policyValueDisplay as Node);
    selection!.removeAllRanges();
    selection!.addRange(range);

    // Copy the policy value to the clipboard.
    navigator.clipboard
        .writeText((policyValueDisplay as CustomElement)!.innerText)
        .catch(error => {
          console.error('Unable to copy policy value to clipboard:', error);
        });
  }

  // Toggle the visibility of an additional row containing the complete text.
  private toggleExpanded() {
    const warningRowDisplay = this.getRequiredElement('.warnings.row');
    const errorRowDisplay = this.getRequiredElement('.errors.row');
    const infoRowDisplay = this.getRequiredElement('.infos.row');
    const valueRowDisplay = this.getRequiredElement('.value.row');
    // <if expr="is_android or is_ios">
    const scopeRowDisplay = this.getRequiredElement('.scope.row');
    scopeRowDisplay.hidden = !scopeRowDisplay.hidden;
    const levelRowDisplay = this.getRequiredElement('.level.row');
    levelRowDisplay.hidden = !levelRowDisplay.hidden;
    // </if>
    valueRowDisplay.hidden = !valueRowDisplay.hidden;
    this.classList!.toggle('expanded', !valueRowDisplay.hidden);

    this.getRequiredElement('.show-more').hidden = !valueRowDisplay.hidden;
    this.getRequiredElement('.show-less').hidden = valueRowDisplay!.hidden;
    if (this.hasWarnings_) {
      warningRowDisplay!.hidden = !warningRowDisplay.hidden;
    }
    if (this.hasErrors_) {
      errorRowDisplay!.hidden = !errorRowDisplay.hidden;
    }
    if (this.hasInfos_) {
      infoRowDisplay!.hidden = !infoRowDisplay.hidden;
    }
    this.shadowRoot!.querySelectorAll<HTMLElement>('.policy-conflict-data')
        .forEach(row => row!.hidden = !row.hidden);
    this.shadowRoot!.querySelectorAll<HTMLElement>('.policy-superseded-data')
        .forEach(row => row!.hidden = !row.hidden);
  }
}
declare global {
  interface HTMLElementTagNameMap {
    'policy-row': PolicyRowElement;
  }
}
customElements.define('policy-row', PolicyRowElement);