// 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);