chromium/chrome/browser/resources/extensions/activity_log/activity_log_stream.ts

// Copyright 2019 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/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_search_field/cr_search_field.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import '../shared_style.css.js';
import './activity_log_stream_item.js';

import type {ChromeEvent} from '/tools/typescript/definitions/chrome_event.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './activity_log_stream.html.js';
import type {StreamItem} from './activity_log_stream_item.js';

export interface ActivityLogEventDelegate {
  getOnExtensionActivity(): ChromeEvent<
      (activity: chrome.activityLogPrivate.ExtensionActivity) => void>;
}

/**
 * Process activity for the stream. In the case of content scripts, we split
 * the activity for every script invoked.
 */
function processActivityForStream(
    activity: chrome.activityLogPrivate.ExtensionActivity): StreamItem[] {
  const activityType = activity.activityType;
  const timestamp = activity.time!;
  const isContentScript = activityType ===
      chrome.activityLogPrivate.ExtensionActivityType.CONTENT_SCRIPT;

  const args = isContentScript ? JSON.stringify([]) : activity.args!;

  let streamItemNames = [activity.apiCall!];

  // TODO(kelvinjiang): Reuse logic from activity_log_history and refactor
  // some of the processing code into a separate file in a follow up CL.
  if (isContentScript) {
    streamItemNames = activity.args ? JSON.parse(activity.args) : [];
    assert(Array.isArray(streamItemNames), 'Invalid data for script names.');
  }

  const other = activity.other;
  const webRequestInfo = other && other.webRequest;

  return streamItemNames.map(name => ({
                               args,
                               argUrl: activity.argUrl!,
                               activityType,
                               name,
                               pageUrl: activity.pageUrl,
                               timestamp,
                               webRequestInfo,
                               expanded: false,
                             }));
}

export class ActivityLogStreamElement extends PolymerElement {
  static get is() {
    return 'activity-log-stream';
  }

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

  static get properties() {
    return {
      extensionId: String,
      delegate: Object,

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

      activityStream_: {
        type: Array,
        value: () => [],
      },

      filteredActivityStream_: {
        type: Array,
        computed:
            'computeFilteredActivityStream_(activityStream_.*, lastSearch_)',
      },

      lastSearch_: {
        type: String,
        value: '',
      },
    };
  }

  extensionId: string;
  delegate: ActivityLogEventDelegate;
  private isStreamOn_: boolean;
  private activityStream_: StreamItem[];
  private filteredActivityStream_: StreamItem[];
  private lastSearch_: string;
  private listenerInstance_:
      (type: chrome.activityLogPrivate.ExtensionActivity) => void;

  constructor() {
    super();

    /**
     * Instance of |extensionActivityListener_| bound to |this|.
     */
    this.listenerInstance_ = () => {};
  }

  override connectedCallback() {
    super.connectedCallback();

    // Since this component is not restamped, this will only be called once
    // in its lifecycle.
    this.listenerInstance_ = this.extensionActivityListener_.bind(this);
    this.startStream();
  }

  private onResizeStream_() {
    this.shadowRoot!.querySelector('iron-list')!.notifyResize();
  }

  clearStream() {
    this.splice('activityStream_', 0, this.activityStream_.length);
  }

  startStream() {
    if (this.isStreamOn_) {
      return;
    }

    this.isStreamOn_ = true;
    this.delegate.getOnExtensionActivity().addListener(this.listenerInstance_);
  }

  pauseStream() {
    if (!this.isStreamOn_) {
      return;
    }

    this.delegate.getOnExtensionActivity().removeListener(
        this.listenerInstance_);
    this.isStreamOn_ = false;
  }

  private onToggleButtonClick_() {
    if (this.isStreamOn_) {
      this.pauseStream();
    } else {
      this.startStream();
    }
  }

  private isStreamEmpty_(): boolean {
    return this.activityStream_.length === 0;
  }

  private isFilteredStreamEmpty_(): boolean {
    return this.filteredActivityStream_.length === 0;
  }

  private shouldShowEmptySearchMessage_(): boolean {
    return !this.isStreamEmpty_() && this.isFilteredStreamEmpty_();
  }

  private extensionActivityListener_(
      activity: chrome.activityLogPrivate.ExtensionActivity) {
    if (activity.extensionId !== this.extensionId) {
      return;
    }

    this.splice(
        'activityStream_', this.activityStream_.length, 0,
        ...processActivityForStream(activity));

    // Used to update the scrollbar.
    this.shadowRoot!.querySelector('iron-list')!.notifyResize();
  }

  private onSearchChanged_(e: CustomEvent<string>) {
    // Remove all whitespaces from the search term, as API call names and
    // URLs should not contain any whitespace. As of now, only single term
    // search queries are allowed.
    const searchTerm = e.detail.replace(/\s+/g, '').toLowerCase();
    if (searchTerm === this.lastSearch_) {
      return;
    }

    this.lastSearch_ = searchTerm;
  }

  private computeFilteredActivityStream_(): StreamItem[] {
    if (!this.lastSearch_) {
      return this.activityStream_.slice();
    }

    // Match on these properties for each activity.
    const propNames = [
      'name',
      'pageUrl',
      'activityType',
    ];

    return this.activityStream_.filter(act => {
      return propNames.some(prop => {
        const value = (act as {[index: string]: any})[prop];
        return value && value.toLowerCase().includes(this.lastSearch_);
      });
    });
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'activity-log-stream': ActivityLogStreamElement;
  }
}

customElements.define(ActivityLogStreamElement.is, ActivityLogStreamElement);