blob: 8138a5f575e3350acdcdd3042040b978a1860fe4 [file] [log] [blame]
// 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/cr_elements/cr_infinite_list/cr_infinite_list.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 {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {FakeChromeEvent} from '../item.js';
import {getCss} from './activity_log_stream.css.js';
import {getHtml} 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>;
}
class DummyActivityLogEventDelegate implements ActivityLogEventDelegate {
getOnExtensionActivity() {
return new FakeChromeEvent();
}
}
/**
* 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 CrLitElement {
static get is() {
return 'activity-log-stream';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
extensionId: {type: String},
delegate: {type: Object},
isStreamOn_: {type: Boolean},
activityStream_: {type: Array},
filteredActivityStream_: {type: Array},
lastSearch_: {type: String},
};
}
accessor extensionId: string = '';
accessor delegate: ActivityLogEventDelegate =
new DummyActivityLogEventDelegate();
protected accessor isStreamOn_: boolean = false;
private accessor activityStream_: StreamItem[] = [];
protected accessor filteredActivityStream_: StreamItem[] = [];
private accessor lastSearch_: string = '';
// Instance of |extensionActivityListener_| bound to |this|.
private listenerInstance_:
(type: chrome.activityLogPrivate.ExtensionActivity) => void = () => {};
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('activityStream_') ||
changedPrivateProperties.has('lastSearch_')) {
this.filteredActivityStream_ = this.computeFilteredActivityStream_();
}
}
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();
}
clearStream() {
this.activityStream_ = [];
}
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;
}
protected onToggleButtonClick_() {
if (this.isStreamOn_) {
this.pauseStream();
} else {
this.startStream();
}
}
protected isStreamEmpty_(): boolean {
return this.activityStream_.length === 0;
}
protected isFilteredStreamEmpty_(): boolean {
return this.filteredActivityStream_.length === 0;
}
protected shouldShowEmptySearchMessage_(): boolean {
return !this.isStreamEmpty_() && this.isFilteredStreamEmpty_();
}
private extensionActivityListener_(
activity: chrome.activityLogPrivate.ExtensionActivity) {
if (activity.extensionId !== this.extensionId) {
return;
}
this.activityStream_ = [
...this.activityStream_,
...processActivityForStream(activity),
];
}
protected 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);