| // 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 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js'; |
| import 'chrome://resources/cr_elements/icons_lit.html.js'; |
| import './icons.html.js'; |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| import type {BigBuffer} from 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js'; |
| import type {Time} from 'chrome://resources/mojo/mojo/public/mojom/base/time.mojom-webui.js'; |
| |
| import {getCss} from './trace_report.css.js'; |
| import {getHtml} from './trace_report.html.js'; |
| import type {ClientTraceReport} from './trace_report.mojom-webui.js'; |
| import {ReportUploadState, SkipUploadReason} from './trace_report.mojom-webui.js'; |
| import {TraceReportBrowserProxy} from './trace_report_browser_proxy.js'; |
| import {Notification, NotificationType} from './trace_report_list.js'; |
| |
| // Create the temporary element here to hold the data to download the trace |
| // since it is only obtained after downloadData_ is called. This way we can |
| // perform a download directly in JS without touching the element that |
| // triggers the action. Initiate download a resource identified by |url| into |
| // |filename|. |
| function downloadUrl(fileName: string, url: string): void { |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = fileName; |
| a.click(); |
| } |
| |
| 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}, |
| isLoading: {type: Boolean}, |
| }; |
| } |
| |
| private traceReportProxy_: TraceReportBrowserProxy = |
| TraceReportBrowserProxy.getInstance(); |
| |
| protected trace: ClientTraceReport = { |
| // Dummy ClientTraceReport |
| uuid: { |
| high: 0n, |
| low: 0n, |
| }, |
| creationTime: {internalValue: 0n}, |
| scenarioName: '', |
| uploadRuleName: '', |
| totalSize: 0n, |
| uploadState: ReportUploadState.kNotUploaded, |
| uploadTime: {internalValue: 0n}, |
| skipReason: SkipUploadReason.kNoSkip, |
| hasTraceContent: false, |
| }; |
| protected isLoading_: boolean = false; |
| |
| protected onCopyUuidClick_(): void { |
| // Get the text field |
| navigator.clipboard.writeText(this.getTokenAsString_()); |
| } |
| |
| protected getTraceSize_(): string { |
| if (this.trace.totalSize < 1) { |
| return '0 Bytes'; |
| } |
| |
| let displayedSize = Number(this.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_(): 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', |
| ]; |
| |
| return skipReasonMap[this.trace.skipReason] ?? |
| 'Could not get the skip reason'; |
| } |
| |
| protected onCopyScenarioClick_(): void { |
| // Get the text field |
| navigator.clipboard.writeText(this.trace.scenarioName); |
| } |
| |
| protected onCopyUploadRuleClick_(): void { |
| // Get the text field |
| navigator.clipboard.writeText(this.trace.uploadRuleName); |
| } |
| |
| protected isManualUploadPermitted_(): boolean { |
| return this.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> { |
| this.isLoading_ = true; |
| const {trace} = |
| await this.traceReportProxy_.handler.downloadTrace(this.trace.uuid); |
| if (trace !== null) { |
| this.downloadData_(`${this.getTokenAsString_()}.gz`, trace); |
| } else { |
| this.dispatchToast_(`Failed to download trace ${this.getTokenAsString_()}.`); |
| } |
| this.isLoading_ = false; |
| } |
| |
| private downloadData_(fileName: string, data: BigBuffer): void { |
| if (data.invalidBuffer) { |
| this.dispatchToast_( |
| `Invalid buffer received for ${this.getTokenAsString_()}.`); |
| return; |
| } |
| try { |
| let bytes: Uint8Array; |
| if (Array.isArray(data.bytes)) { |
| bytes = new Uint8Array(data.bytes); |
| } else { |
| assert(!!data.sharedMemory, 'sharedMemory must be defined here'); |
| const sharedMemory = data.sharedMemory!; |
| const {buffer, result} = |
| sharedMemory.bufferHandle.mapBuffer(0, sharedMemory.size); |
| assert(result === Mojo.RESULT_OK, 'Could not map buffer'); |
| bytes = new Uint8Array(buffer); |
| } |
| const url = URL.createObjectURL( |
| new Blob([bytes], {type: 'application/octet-stream'})); |
| downloadUrl(fileName, url); |
| } catch (e) { |
| this.dispatchToast_( |
| `Unable to create blob from trace data for ${this.getTokenAsString_()}.`); |
| } |
| } |
| |
| protected async onDeleteTraceClick_(): Promise<void> { |
| this.isLoading_ = true; |
| const {success} = |
| await this.traceReportProxy_.handler.deleteSingleTrace(this.trace.uuid); |
| if (!success) { |
| this.dispatchToast_(`Failed to delete ${this.getTokenAsString_()}.`); |
| } else { |
| this.dispatchReloadRequest_(); |
| } |
| this.isLoading_ = false; |
| } |
| |
| protected async onUploadTraceClick_(): Promise<void> { |
| this.isLoading_ = true; |
| const {success} = |
| await this.traceReportProxy_.handler.userUploadSingleTrace( |
| this.trace.uuid); |
| if (!success) { |
| this.dispatchToast_(`Failed to upload trace ${this.getTokenAsString_()}.`); |
| } else { |
| this.dispatchReloadRequest_(); |
| } |
| this.isLoading_ = false; |
| } |
| |
| protected uploadStateEqual_(state: ReportUploadState): boolean { |
| return this.trace.uploadState === state; |
| } |
| |
| protected getTokenAsString_(): string { |
| return `${this.trace.uuid.high.toString(16)}-${ |
| this.trace.uuid.low.toString(16)}`; |
| } |
| |
| private dispatchToast_(message: string): void { |
| this.dispatchEvent(new CustomEvent('show-toast', { |
| bubbles: true, |
| composed: true, |
| detail: new Notification(NotificationType.ERROR, message), |
| })); |
| } |
| |
| protected isDownloadDisabled_(): boolean { |
| return this.isLoading_ || !this.trace.hasTraceContent; |
| } |
| |
| protected getDownloadTooltip_(): string { |
| return this.trace.hasTraceContent ? 'Download Trace' : 'Trace expired'; |
| } |
| |
| private dispatchReloadRequest_(): void { |
| this.fire('refresh-traces-request'); |
| } |
| |
| protected getStateCssClass_(): string { |
| switch (this.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_(): string { |
| switch (this.trace.uploadState) { |
| case ReportUploadState.kNotUploaded: |
| return `Skip reason: ${this.getSkipReason_()}`; |
| case ReportUploadState.kPending: |
| return 'Pending upload'; |
| case ReportUploadState.kPending_UserRequested: |
| return 'Pending upload: User requested'; |
| case ReportUploadState.kUploaded: |
| return `Uploaded: ${this.dateToString_(this.trace.uploadTime)}`; |
| default: |
| return ''; |
| } |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'trace-report': TraceReportElement; |
| } |
| } |
| |
| customElements.define(TraceReportElement.is, TraceReportElement); |