chromium/chrome/browser/resources/chromeos/launcher_internals/results_table.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 {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {Result} from './launcher_internals.mojom-webui.js';
import {getTemplate} from './results_table.html.js';

export interface LauncherResultsTableElement {
  $: {
    'headerRow': HTMLTableRowElement,
    'resultsSection': HTMLTableSectionElement,
    'displayScoreHeader': HTMLTableCellElement,
  };
}

export class LauncherResultsTableElement extends PolymerElement {
  static get is() {
    return 'launcher-results-table';
  }

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

  // Current results keyed by result id.
  private results: Map<string, Result> = new Map();

  // Extra header cells, keyed by their text content. These are placed into the
  // header row in insertion order.
  private headerCells: Map<string, HTMLTableCellElement> = new Map();

  // The result property used to sort the table. 'Display score' is the default
  // key, and this will change whenever the user clicks on a new header to sort
  // by.
  private sortKey: string = 'Display score';

  // The IDs of results that are currently selected. This is used to persist
  // formatting when the table is sorted.
  private selectedIds: Set<string> = new Set();

  override connectedCallback() {
    super.connectedCallback();
    this.$.displayScoreHeader.addEventListener(
        'click',
        () => this.sortTable('Display score', /*resultsChanged=*/ false));
  }

  clearResults() {
    this.results.clear();
    this.$.resultsSection.innerHTML =
        window.trustedTypes ? (window.trustedTypes.emptyHTML) : '';
    for (const cell of this.headerCells.values()) {
      this.$.headerRow.removeChild(cell);
    }
    this.headerCells.clear();
  }

  addResults(newResults: Result[]) {
    for (const result of newResults) {
      this.results.set(result.id, result);
      this.addHeaders(Object.keys(result.rankerScores));
    }
    this.sortTable(this.sortKey, /*resultsChanged=*/ true);
  }

  // Appends any new headers to the end of the header row. All new headers
  // should support sort-on-click.
  private addHeaders(newHeaders: string[]) {
    for (const header of newHeaders) {
      if (this.headerCells.has(header)) {
        continue;
      }
      const newCell = this.$.headerRow.insertCell();
      newCell.textContent = header;
      newCell.className = 'sort-header';
      newCell.addEventListener(
          'click', () => this.sortTable(header, /*resultsChanged=*/ false));
      this.headerCells.set(header, newCell);
    }
  }

  // Repopulates the table with results sorted by the current key in descending
  // order.
  private sortTable(sortKey: string, resultsChanged: boolean) {
    if (!resultsChanged && this.sortKey === sortKey) {
      return;
    }
    this.sortKey = sortKey;

    const sortedResults = Array.from(this.results.values());
    if (this.sortKey === 'Display score') {
      sortedResults.sort((a, b) => b.score - a.score);
    } else {
      const getSortValue = (result: Result): number => {
        const value = result.rankerScores[this.sortKey];
        return value === undefined ? 0 : value;
      };
      sortedResults.sort((a, b) => getSortValue(b) - getSortValue(a));
    }

    // Clear and repopulate the results table.
    this.$.resultsSection.innerHTML =
        window.trustedTypes ? (window.trustedTypes.emptyHTML) : '';
    for (const result of sortedResults) {
      const newRow = this.$.resultsSection.insertRow();
      newRow.addEventListener('click', (e: Event) => this.toggleRowSelected(e));
      if (this.selectedIds.has(result.id)) {
        newRow.classList.add('selected');
      }
      [result.id,
       result.title,
       result.description,
       result.resultType,
       result.metricsType,
       result.displayType,
       result.score.toString(),
       ...this.flattenScores(result.rankerScores),
      ].forEach(field => {
        const newCell = newRow.insertCell();
        newCell.textContent = field;
      });
    }
  }

  // Converts ranker scores into an array of scores in string form and ordered
  // according to the current headers.
  private flattenScores(inputScores: {[key: string]: number}): string[] {
    const outputScores = [];
    for (const header of this.headerCells.keys()) {
      const score = inputScores[header];
      outputScores.push(score === undefined ? '' : score.toString());
    }
    return outputScores;
  }

  // Toggles selection in the class list of the targeted row.
  private toggleRowSelected(event: Event) {
    const row = (event.target as HTMLElement).closest('tr');
    if (row == null || row.cells.length === 0) {
      return;
    }

    const id = row.cells[0]!.textContent!;
    if (row.classList.contains('selected')) {
      row.classList.remove('selected');
      this.selectedIds.delete(id);
    } else {
      row.classList.add('selected');
      this.selectedIds.add(id);
    }
  }
}

customElements.define(
    LauncherResultsTableElement.is, LauncherResultsTableElement);