blob: 389b41059224326086fbadce7c6921a567b15a87 [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 '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '//resources/cr_elements/icons.html.js';
import './icons.html.js';
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';
import type {BigBuffer} from '//resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js';
import type {Time} from '//resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';
import type {Token} from '//resources/mojo/mojo/public/mojom/base/token.mojom-webui.js';
import {getCss} from './trace_report.css.js';
import {getHtml} from './trace_report.html.js';
import {Notification, NotificationType} from './trace_report_list.js';
import {downloadTraceData, getTokenAsUuidString} from './trace_util.js';
import {TracesBrowserProxy} from './traces_browser_proxy.js';
import type {ClientTraceReport} from './traces_internals.mojom-webui.js';
import {ReportUploadState, SkipUploadReason} from './traces_internals.mojom-webui.js';
export class TraceReportElement extends CrLitElement {
static get is() {
return 'trace-report';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
trace: {type: Object},
isHeader: {type: Boolean},
isLoading: {type: Boolean},
};
}
private traceReportProxy_: TracesBrowserProxy =
TracesBrowserProxy.getInstance();
protected accessor trace: ClientTraceReport|null = null;
protected accessor isHeader: boolean = false;
protected accessor isLoading: boolean = false;
protected onCopyUuidClick_(): void {
if (!this.trace) {
return;
}
// Get the text field
navigator.clipboard.writeText(getTokenAsUuidString(this.trace.uuid));
}
protected getTraceSize_(trace: ClientTraceReport): string {
if (trace.totalSize < 1) {
return '0 Bytes';
}
let displayedSize = Number(trace.totalSize);
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
let i = 0;
for (i; displayedSize >= k && i < 3; i++) {
displayedSize /= k;
}
return `${displayedSize.toFixed(2)} ${sizes[i]}`;
}
protected getSkipReason_(trace: ClientTraceReport): string {
// Keep this in sync with the values of SkipUploadReason in
// tracereport.mojom
const skipReasonMap: string[] = [
'None',
'Size limit exceeded',
'Not anonymized',
'Scenario quota exceeded',
'Upload timed out',
'Local scenario',
];
return skipReasonMap[trace.skipReason] ?? 'Could not get the skip reason';
}
protected onCopyScenarioClick_(): void {
if (!this.trace) {
return;
}
// Get the text field
navigator.clipboard.writeText(this.trace.scenarioName);
}
protected onCopyUploadRuleClick_(): void {
if (!this.trace) {
return;
}
// Get the text field
navigator.clipboard.writeText(this.trace.uploadRuleName);
}
protected isManualUploadDisabled_(trace: ClientTraceReport): boolean {
return this.isLoading ||
trace.skipReason === SkipUploadReason.kNotAnonymized;
}
protected dateToString_(mojoTime: Time): string {
// The JS Date() is based off of the number of milliseconds since
// the UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue|
// of the base::Time (represented in mojom.Time) represents the
// number of microseconds since the Windows FILETIME epoch
// (1601-01-01 00:00:00 UTC). This computes the final JS time by
// computing the epoch delta and the conversion from microseconds to
// milliseconds.
const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
// |epochDeltaInMs| equals to
// base::Time::kTimeTToMicrosecondsOffset.
const epochDeltaInMs = unixEpoch - windowsEpoch;
const timeInMs = Number(mojoTime.internalValue) / 1000;
// Define the format in which the date string is going to be displayed.
return new Date(timeInMs - epochDeltaInMs)
.toLocaleString(
/*locales=*/ undefined, {
hour: 'numeric',
minute: 'numeric',
month: 'short',
day: 'numeric',
year: 'numeric',
hour12: true,
});
}
protected async onDownloadTraceClick_(): Promise<void> {
if (!this.trace) {
return;
}
this.isLoading = true;
const {trace} =
await this.traceReportProxy_.handler.downloadTrace(this.trace.uuid);
if (trace !== null) {
this.downloadData_(trace, this.trace.uuid);
} else {
this.dispatchToast_(
`Failed to download trace ${getTokenAsUuidString(this.trace.uuid)}.`);
}
this.isLoading = false;
}
private downloadData_(data: BigBuffer, uuid: Token): void {
if (data.invalidBuffer) {
this.dispatchToast_(
`Invalid buffer received for ${getTokenAsUuidString(uuid)}.`);
return;
}
try {
downloadTraceData(data, uuid);
} catch (e) {
this.dispatchToast_(`Unable to create blob from trace data for ${
getTokenAsUuidString(uuid)}.`);
}
}
protected async onDeleteTraceClick_(): Promise<void> {
if (!this.trace) {
return;
}
this.isLoading = true;
const {success} =
await this.traceReportProxy_.handler.deleteSingleTrace(this.trace.uuid);
if (!success) {
this.dispatchToast_(
`Failed to delete ${getTokenAsUuidString(this.trace.uuid)}.`);
} else {
this.dispatchReloadRequest_();
}
}
protected async onUploadTraceClick_(): Promise<void> {
if (!this.trace) {
return;
}
this.isLoading = true;
const {success} =
await this.traceReportProxy_.handler.userUploadSingleTrace(
this.trace.uuid);
if (!success) {
this.dispatchToast_(
`Failed to upload trace ${getTokenAsUuidString(this.trace.uuid)}.`);
} else {
this.dispatchReloadRequest_();
}
this.isLoading = false;
}
protected uploadStateEqual_(
trace: ClientTraceReport, state: ReportUploadState): boolean {
return trace.uploadState === state;
}
private dispatchToast_(message: string): void {
this.dispatchEvent(new CustomEvent('show-toast', {
bubbles: true,
composed: true,
detail: new Notification(NotificationType.ERROR, message),
}));
}
protected isDownloadDisabled_(trace: ClientTraceReport): boolean {
return this.isLoading || !trace.hasTraceContent;
}
protected getDownloadTooltip_(trace: ClientTraceReport): string {
return trace.hasTraceContent ? 'Download Trace' : 'Trace expired';
}
private dispatchReloadRequest_(): void {
this.fire('refresh-traces-request');
}
protected getStateCssClass_(trace: ClientTraceReport): string {
switch (trace.uploadState) {
case ReportUploadState.kNotUploaded:
return 'state-default';
case ReportUploadState.kPending:
case ReportUploadState.kPending_UserRequested:
return 'state-pending';
case ReportUploadState.kUploaded:
return 'state-success';
default:
return '';
}
}
protected getStateText_(trace: ClientTraceReport): string {
switch (trace.uploadState) {
case ReportUploadState.kNotUploaded:
return `Upload skipped: ${this.getSkipReason_(trace)}`;
case ReportUploadState.kPending:
return 'Pending upload';
case ReportUploadState.kPending_UserRequested:
return 'Pending upload: User requested';
case ReportUploadState.kUploaded:
return `Uploaded: ${this.dateToString_(trace.uploadTime)}`;
default:
return '';
}
}
}
declare global {
interface HTMLElementTagNameMap {
'trace-report': TraceReportElement;
}
}
customElements.define(TraceReportElement.is, TraceReportElement);