| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert} from 'chrome://resources/js/assert.js'; |
| import {CustomElement} from 'chrome://resources/js/custom_element.js'; |
| |
| import {getTemplate} from './app.html.js'; |
| import {StructuredMetricsBrowserProxyImpl} from './structured_metrics_browser_proxy.js'; |
| import type {SearchParams, StructuredMetricEvent, StructuredMetricsSummary} from './structured_utils.js'; |
| import {updateStructuredMetricsEvents, updateStructuredMetricsSummary} from './structured_utils.js'; |
| |
| /** |
| * Gets search params from search url. |
| */ |
| function getSearchParams(): string { |
| return window.location.search.substring(1); |
| } |
| |
| export class StructuredMetricsInternalsAppElement extends CustomElement { |
| static get is(): string { |
| return 'structured-metrics-internals-app'; |
| } |
| |
| static override get template() { |
| return getTemplate(); |
| } |
| |
| private browserProxy_: StructuredMetricsBrowserProxyImpl = |
| StructuredMetricsBrowserProxyImpl.getInstance(); |
| private summaryIntervalId_: ReturnType<typeof setInterval>; |
| |
| initPromise: Promise<void>; |
| |
| textSearch: HTMLInputElement; |
| |
| searchQuery: SearchParams|null = null; |
| |
| searchError: boolean = false; |
| |
| constructor() { |
| super(); |
| |
| // This must be set before |initSearchParams_| is called. |
| this.textSearch = this.getRequiredElement<HTMLInputElement>('#search-bar'); |
| this.initSearchParams_(); |
| |
| this.initPromise = this.init_(); |
| |
| // Create periodic callbacks. |
| this.summaryIntervalId_ = |
| setInterval(() => this.updateStructuredMetricsSummary_(), 5000); |
| } |
| |
| disconnectedCallback() { |
| clearInterval(this.summaryIntervalId_); |
| } |
| |
| private async init_(): Promise<void> { |
| // Fetch Structured Metrics summary and events. |
| // TODO: Implement a push model as new events are recorded. |
| await this.updateStructuredMetricsSummary_(); |
| await this.updateStructuredMetricsEvents_(); |
| |
| const eventRefreshButton = this.getRequiredElement('#sm-refresh-events'); |
| eventRefreshButton.addEventListener( |
| 'click', () => this.updateStructuredMetricsEvents_()); |
| } |
| |
| /** |
| * Fetches summary information of the Structured Metrics service and renders |
| * it. |
| */ |
| private async updateStructuredMetricsSummary_(): Promise<void> { |
| const summary: StructuredMetricsSummary = |
| await this.browserProxy_.fetchStructuredMetricsSummary(); |
| const template = |
| this.getRequiredElement<HTMLTemplateElement>('#summary-row-template'); |
| const smSummaryBody = this.getRequiredElement('#sm-summary-body'); |
| updateStructuredMetricsSummary(smSummaryBody, summary, template); |
| } |
| |
| /** |
| * Fetches all events currently recorded by the Structured Metrics Service and |
| * renders them. It an event has been uploaded then it will not be shown |
| * again. This only shows Events recorded in Chromium. Platform2 events are |
| * not supported yet. |
| */ |
| private async updateStructuredMetricsEvents_(): Promise<void> { |
| const events: StructuredMetricEvent[] = |
| await this.browserProxy_.fetchStructuredMetricsEvents(); |
| const eventTemplate = this.getRequiredElement<HTMLTemplateElement>( |
| '#structured-metrics-event-row-template'); |
| const eventDetailsTemplate = this.getRequiredElement<HTMLTemplateElement>( |
| '#structured-metrics-event-details-template'); |
| |
| const kvTemplate = |
| this.getRequiredElement<HTMLTemplateElement>('#summary-row-template'); |
| const eventTableBody = this.getRequiredElement('#sm-events-body'); |
| |
| updateStructuredMetricsEvents( |
| eventTableBody, events, this.searchQuery, eventTemplate, |
| eventDetailsTemplate, kvTemplate); |
| } |
| |
| /** |
| * Initializes search params from the URL. |
| */ |
| private initSearchParams_() { |
| const searchString = this.sanitizeUrlToSearch_(); |
| this.searchQuery = this.parseSearchString_(searchString); |
| |
| this.textSearch.value = searchString; |
| this.textSearch.addEventListener('search', () => { |
| this.updateSearchCriteria_(); |
| }); |
| } |
| |
| |
| /** |
| * Updates the windows search url. |
| */ |
| private updateSearchCriteria_() { |
| // Update the url to reflect the search string. This will redirect the new |
| // url page, updating the searchQuery. |
| window.location.search = '?' + this.sanitizeSearchToUrl_(); |
| } |
| |
| /** |
| * Sanitize the search format into a valid format for the URL. |
| */ |
| private sanitizeSearchToUrl_(): string { |
| return this.textSearch.value.replace(/\s+/gi, '&').replace(/:/gi, '='); |
| } |
| |
| /** |
| * Sanitize the URL search parameters into the search format. |
| */ |
| private sanitizeUrlToSearch_(): string { |
| return getSearchParams().replace(/&/gi, ' ').replace(/=/gi, ':'); |
| } |
| |
| /** |
| * Parse search format into an object. |
| * |
| * The format is a space separated lists of "key:value" pairs. Currently, a |
| * single search term is not supported. |
| */ |
| private parseSearchString_(text: string): SearchParams|null { |
| // Page should be rebuilt on a new search query, but leaving it because it |
| // is better to be safe then have an error message that doesn't disappear |
| // when the page is refreshed.. |
| if (this.searchError) { |
| this.hideSearchErrorMessage_(); |
| } |
| |
| if (text.length === 0) { |
| return null; |
| } |
| |
| // If an ':' is not found then we are doing a full text search. The string |
| // is the query as is. |
| if (text.indexOf(':') === -1) { |
| return null; |
| } |
| |
| // If it is found, then we are doing a categorical search, this parses the |
| // string into a map of category and search value. |
| const categories = new Map<string, string>(); |
| const validSearchKeys = ['project', 'event', 'metric']; |
| |
| text.split(' ').forEach((cat) => { |
| const [key, value] = cat.split(':', 2); |
| if (key !== undefined && value !== undefined) { |
| if (validSearchKeys.find((value) => value === key) === undefined) { |
| this.setSearchErrorMessage_(`invalid search key: ${ |
| key}. valid keys are project, event, metric`); |
| return; |
| } |
| categories.set(key, value); |
| } |
| }); |
| |
| return categories; |
| } |
| |
| /** |
| * Hides the search error message. |
| */ |
| private hideSearchErrorMessage_(): void { |
| this.searchError = false; |
| |
| const errorMsg = |
| this.getRequiredElement<HTMLParagraphElement>('#search-error-msg'); |
| assert(errorMsg); |
| errorMsg.style.display = 'none'; |
| } |
| |
| /** |
| * Sets and shows the error message. |
| */ |
| private setSearchErrorMessage_(msg: string): void { |
| this.searchError = true; |
| |
| // Set the content of the error message. |
| const errorMsg = |
| this.getRequiredElement<HTMLParagraphElement>('#search-error-msg'); |
| assert(errorMsg); |
| errorMsg.innerText = msg; |
| |
| // Shows the error message |
| errorMsg.style.display = 'block'; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'structured-metrics-internals-app': StructuredMetricsInternalsAppElement; |
| } |
| } |
| |
| customElements.define( |
| StructuredMetricsInternalsAppElement.is, |
| StructuredMetricsInternalsAppElement); |