// 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; initPromise: Promise; textSearch: HTMLInputElement; searchQuery: SearchParams|null = null; searchError: boolean = false; constructor() { super(); // This must be set before |initSearchParams_| is called. this.textSearch = this.getRequiredElement('#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 { // 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 { const summary: StructuredMetricsSummary = await this.browserProxy_.fetchStructuredMetricsSummary(); const template = this.getRequiredElement('#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 { const events: StructuredMetricEvent[] = await this.browserProxy_.fetchStructuredMetricsEvents(); const eventTemplate = this.getRequiredElement( '#structured-metrics-event-row-template'); const eventDetailsTemplate = this.getRequiredElement( '#structured-metrics-event-details-template'); const kvTemplate = this.getRequiredElement('#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(); 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('#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('#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);