blob: 0317a3b1fc3c9d56c18f0b3a5c7002802cc403e4 [file] [log] [blame]
// 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 './icons.html.js';
import './request.js';
import '//resources/cr_elements/cr_button/cr_button.js';
import '//resources/cr_elements/cr_dialog/cr_dialog.js';
import '//resources/cr_elements/cr_drawer/cr_drawer.js';
import '//resources/cr_elements/cr_icon/cr_icon.js';
import '//resources/cr_elements/cr_link_row/cr_link_row.js';
import '//resources/cr_elements/cr_textarea/cr_textarea.js';
import '//resources/cr_elements/cr_input/cr_input.js';
import '//resources/cr_elements/cr_toast/cr_toast.js';
import '//resources/cr_elements/cr_toolbar/cr_toolbar.js';
import type {CrDialogElement} from '//resources/cr_elements/cr_dialog/cr_dialog.js';
import type {CrDrawerElement} from '//resources/cr_elements/cr_drawer/cr_drawer.js';
import type {CrToastElement} from '//resources/cr_elements/cr_toast/cr_toast.js';
import type {CrToolbarSearchFieldElement} from '//resources/cr_elements/cr_toolbar/cr_toolbar_search_field.ts';
import type {TimeDelta} from '//resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';
import {assert} from 'chrome://resources/js/assert.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {getCss} from './app.css.js';
import {getHtml} from './app.html.js';
import type {PageHandlerInterface, Request} from './suggest_internals.mojom-webui.js';
import {PageCallbackRouter, PageHandler} from './suggest_internals.mojom-webui.js';
interface SuggestInternalsAppElement {
$: {
drawer: CrDrawerElement,
fileInput: HTMLInputElement,
hardcodeResponseDialog: CrDialogElement,
toast: CrToastElement,
};
}
// Displays the suggest requests from the most recent to the least recent.
class SuggestInternalsAppElement extends CrLitElement {
static get is() {
return 'suggest-internals-app';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
filter_: {type: String},
hardcodedRequest_: {type: Object},
requests_: {type: Array},
responseDelay_: {type: String},
responseText_: {type: String},
toastDuration_: {type: Number},
toastMessage_: {type: String},
};
}
private accessor filter_: string = '';
protected accessor hardcodedRequest_: Request|null = null;
protected accessor requests_: Request[] = [];
protected accessor responseDelay_: string = '';
protected accessor responseText_: string = '';
protected accessor toastDuration_: number = 3000;
protected accessor toastMessage_: string = '';
private callbackRouter_: PageCallbackRouter;
private pageHandler_: PageHandlerInterface;
private suggestionsRequestCompletedListenerId_: number|null = null;
private suggestionsRequestCreatedListenerId_: number|null = null;
private suggestionsRequestStartedListenerId_: number|null = null;
constructor() {
super();
this.pageHandler_ = PageHandler.getRemote();
this.callbackRouter_ = new PageCallbackRouter();
this.pageHandler_.setPage(
this.callbackRouter_.$.bindNewPipeAndPassRemote());
}
override connectedCallback() {
super.connectedCallback();
this.suggestionsRequestCreatedListenerId_ =
this.callbackRouter_.onRequestCreated.addListener(
this.onRequestCreated_.bind(this));
this.suggestionsRequestStartedListenerId_ =
this.callbackRouter_.onRequestStarted.addListener(
this.onRequestStarted_.bind(this));
this.suggestionsRequestCompletedListenerId_ =
this.callbackRouter_.onRequestCompleted.addListener(
this.onRequestCompleted_.bind(this));
}
override disconnectedCallback() {
super.disconnectedCallback();
assert(this.suggestionsRequestCreatedListenerId_);
this.callbackRouter_.removeListener(
this.suggestionsRequestCreatedListenerId_);
assert(this.suggestionsRequestStartedListenerId_);
this.callbackRouter_.removeListener(
this.suggestionsRequestStartedListenerId_);
assert(this.suggestionsRequestCompletedListenerId_);
this.callbackRouter_.removeListener(
this.suggestionsRequestCompletedListenerId_);
}
private millisecondsToMojoTimeDelta(milliseconds: number): TimeDelta {
return {microseconds: BigInt(Math.floor(milliseconds * 1000))};
}
protected onClearClick_() {
this.requests_ = [];
this.hardcodedRequest_ = null;
}
protected onCloseDialogs_() {
this.$.hardcodeResponseDialog.close();
}
protected async onConfirmHardcodeResponseDialog_() {
const responseDelayMs = Math.max(0, parseInt(this.responseDelay_) || 0);
await this.pageHandler_
.hardcodeResponse(
this.responseText_,
this.millisecondsToMojoTimeDelta(responseDelayMs))
.then(({request}) => {
this.hardcodedRequest_ = request;
});
this.$.hardcodeResponseDialog.close();
}
protected onExportClick_() {
const a = document.createElement('a');
const file =
new Blob([this.stringifyRequests_()], {type: 'application/json'});
a.href = URL.createObjectURL(file);
const iso = (new Date()).toISOString();
iso.replace(/:/g, '').split('.')[0];
a.download = `suggest_internals_export_${iso}.json`;
a.click();
}
protected onFilterChanged_(e: CustomEvent<string>) {
this.filter_ = e.detail ?? '';
}
protected onImportClick_() {
this.$.fileInput.click();
}
protected onImportFile_(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) {
return;
}
this.readFile(file).then((importString: string) => {
try {
this.requests_ = JSON.parse(importString);
} catch (error) {
console.error('error during import, invalid json:', error);
}
});
}
protected onOpenHardcodeResponseDialog_(e: CustomEvent<string>) {
this.responseDelay_ = '';
this.responseText_ = e.detail;
this.$.hardcodeResponseDialog.showModal();
}
protected onShowToast_(e: CustomEvent<string>) {
this.toastMessage_ = e.detail;
this.$.toast.show();
}
private onRequestCreated_(request: Request) {
// Add the request to the start of the list of known requests.
this.requests_.unshift(request);
this.requestUpdate();
}
private onRequestStarted_(request: Request) {
const index = this.requests_.findIndex((element: Request) => {
return request.id === element.id;
});
// If the request is known, update it with the additional information.
if (index !== -1) {
this.requests_[index]!.status = request.status;
this.requests_[index]!.data =
Object.assign({}, this.requests_[index]!.data, request.data);
this.requests_[index]!.startTime = request.startTime;
this.requestUpdate();
}
}
private onRequestCompleted_(request: Request) {
const index = this.requests_.findIndex((element: Request) => {
return request.id === element.id;
});
// If the request is known, update it with the additional information.
if (index !== -1) {
this.requests_[index]!.status = request.status;
this.requests_[index]!.data =
Object.assign({}, this.requests_[index]!.data, request.data);
this.requests_[index]!.endTime = request.endTime;
this.requests_[index]!.response = request.response;
this.requestUpdate();
}
}
private readFile(file: File): Promise<string> {
return new Promise(resolve => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.readyState === FileReader.DONE) {
resolve(reader.result as string);
} else {
console.error('error importing, unable to read file:', reader.error);
}
};
reader.readAsText(file);
});
}
protected requestFilter_(request: Request): boolean {
const filter = this.filter_.trim().toLowerCase();
return request.url.url.toLowerCase().includes(filter);
}
protected showOutputControls_() {
this.$.drawer.openDrawer();
}
private stringifyRequests_() {
return JSON.stringify(
this.requests_,
(_key, value) => typeof value === 'bigint' ? value.toString() : value);
}
protected populateSearchInput_(e: CustomEvent<string>) {
// Populate the searchbar with the pgcl of the selected chip.
const toolbar = this.shadowRoot.querySelector<HTMLElement>('cr-toolbar')!;
const searchbar =
toolbar.shadowRoot!.querySelector<CrToolbarSearchFieldElement>(
'cr-toolbar-search-field')!;
searchbar.setValue('pgcl=' + e.detail);
}
protected onResponseDelayChanged_(e: CustomEvent<{value: string}>) {
this.responseDelay_ = e.detail.value;
}
protected onResponseTextChanged_(e: CustomEvent<{value: string}>) {
this.responseText_ = e.detail.value;
}
}
export type AppElement = SuggestInternalsAppElement;
declare global {
interface HTMLElementTagNameMap {
'suggest-internals-app': SuggestInternalsAppElement;
}
}
customElements.define(
SuggestInternalsAppElement.is, SuggestInternalsAppElement);