commit b93e40e1e23baa885fd419a45d574efe80c87547
parent 45f78eb8a4ce94ff423616cb3f0b286e16391541
Author: Charlie Humphreys <chumphreys@mozilla.com>
Date: Thu, 16 Oct 2025 18:35:58 +0000
Bug 1992754: Add settings pane for about:glean metrics table and charts r=TravisLong,fluent-reviewers,toolkit-telemetry-reviewers,bolsson
Differential Revision: https://phabricator.services.mozilla.com/D267728
Diffstat:
5 files changed, 510 insertions(+), 53 deletions(-)
diff --git a/toolkit/content/aboutGlean.css b/toolkit/content/aboutGlean.css
@@ -72,13 +72,53 @@ body {
overflow-y: scroll;
}
+#metrics-table-header {
+ display: flex;
+ justify-content: space-between;
+}
+
+#metrics-table-settings:not([hidden="true"]) .content {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+#metrics-table-settings .content > * {
+ flex: 1 0 auto;
+}
+
+@media (max-width: 1096px) {
+ #metrics-table-settings .content > * {
+ flex-basis: 100%;
+ }
+}
+
+#metrics-table-settings,
+#metrics-table-settings .box {
+ margin-bottom: 5px;
+ padding: 10px;
+ border: 1px solid var(--border-color-card);
+ border-radius: var(--border-radius-small);
+ background-color: var(--background-color-box);
+}
+
+#metrics-table-settings h2,
+#metrics-table-settings h3,
+#metrics-table-settings h4 {
+ margin: 10px 0;
+}
+
+#metrics-table-settings hr {
+ margin: 20px 0;
+}
+
td[data-d3-cell="actions"] > div {
display: flex;
justify-content: center;
align-items: center;
}
-td[data-d3-cell="value"] > pre {
+td[data-d3-cell="value"] > pre,
+#metrics-table-settings-timeline-example > pre {
background-color: var(--background-color-box);
padding: 5px;
border-radius: var(--border-radius-small);
@@ -86,13 +126,19 @@ td[data-d3-cell="value"] > pre {
width: fit-content;
}
-td[data-d3-cell="value"] > svg {
+td[data-d3-cell="value"] > svg,
+.chart-display > svg {
color: var(--text-color);
border: 1px solid var(--border-color-card);
border-radius: var(--border-radius-small);
background-color: var(--background-color-box);
}
+.vertical-flex {
+ display: flex;
+ flex-direction: column;
+}
+
.histogram .boxes rect,
.timeline .events circle {
fill: var(--color-accent-primary);
@@ -118,7 +164,6 @@ td[data-d3-cell="value"] > svg {
stroke: var(--text-color);
}
-svg.timeline,
pre.withChart {
max-width: 500px;
overflow: scroll;
diff --git a/toolkit/content/aboutGlean.html b/toolkit/content/aboutGlean.html
@@ -135,7 +135,85 @@
</ul>
</div>
<div id="metrics-table" class="tab" hidden="true">
- <h2 data-l10n-id="about-glean-metrics-table-header"></h2>
+ <div id="metrics-table-header">
+ <h2 data-l10n-id="about-glean-metrics-table-header"></h2>
+ <button name="metrics-table-settings-button" id="metrics-table-settings-button" data-l10n-id="about-glean-metrics-table-settings-button"></button>
+ </div>
+ <div id="metrics-table-settings" hidden="true">
+ <h2 data-l10n-id="about-glean-metrics-table-settings-title"></h2>
+ <div class="content">
+ <div>
+ <h3 data-l10n-id="about-glean-metrics-table-settings-category-general"></h3>
+ <input type="checkbox" name="settings-hide-empty-value-rows" id="settings-hide-empty-value-rows" data-form-control="hideEmptyValueRows"/>
+ <label for="settings-hide-empty-value-rows" data-l10n-id="about-glean-metrics-table-settings-hide-empty-value-rows"></label>
+ </div>
+ <div>
+ <h3 data-l10n-id="about-glean-metrics-table-settings-category-visualizations"></h3>
+ <div class="box" data-form-group="histograms">
+ <h4 data-l10n-id="about-glean-metrics-table-settings-category-visualizations-histogram"></h4>
+
+ <div class="vertical-flex">
+ <div>
+ <label for="settings-histograms-box-padding" data-l10n-id="about-glean-metrics-table-settings-histograms-box-padding"></label>
+ <input name="settings-histograms-box-padding" id="settings-histograms-box-padding" type="number" data-form-control="boxPadding"/>
+ </div>
+ <div>
+ <label for="settings-histograms-left-padding" data-l10n-id="about-glean-metrics-table-settings-histograms-left-padding"></label>
+ <input name="settings-histograms-left-padding" id="settings-histograms-left-padding" type="number" data-form-control="leftPadding"/>
+ </div>
+ <div>
+ <label for="settings-histograms-chart-padding" data-l10n-id="about-glean-metrics-table-settings-histograms-chart-padding"></label>
+ <input name="settings-histograms-chart-padding" id="settings-histograms-chart-padding" type="number" data-form-control="chartPadding"/>
+ </div>
+ <div>
+ <label for="settings-histograms-scaled-max" data-l10n-id="about-glean-metrics-table-settings-histograms-scaled-max"></label>
+ <input name="settings-histograms-scaled-max" id="settings-histograms-scaled-max" type="number" data-form-control="scaledMax"/>
+ </div>
+ <div>
+ <label for="settings-histograms-chart-max" data-l10n-id="about-glean-metrics-table-settings-histograms-chart-max"></label>
+ <input name="settings-histograms-chart-max" id="settings-histograms-chart-max" type="number" data-form-control="chartMax"/>
+ </div>
+ </div>
+
+ <h4 data-l10n-id="about-glean-metrics-table-settings-visualization-example"></h4>
+ <div id="metrics-table-settings-histogram-example" class="chart-display"></div>
+ </div>
+ <div class="box" data-form-group="timelines">
+ <h4 data-l10n-id="about-glean-metrics-table-settings-category-visualizations-timeline"></h4>
+
+ <div class="vertical-flex">
+ <div>
+ <label for="settings-timelines-height" data-l10n-id="about-glean-metrics-table-settings-timelines-height"></label>
+ <input name="settings-timelines-height" id="settings-timelines-height" type="number" data-form-control="height"/>
+ </div>
+ <div>
+ <label for="settings-timelines-width" data-l10n-id="about-glean-metrics-table-settings-timelines-width"></label>
+ <input name="settings-timelines-width" id="settings-timelines-width" type="number" data-form-control="width"/>
+ </div>
+ <div>
+ <label for="settings-timelines-chart-padding" data-l10n-id="about-glean-metrics-table-settings-timelines-chart-padding"></label>
+ <input name="settings-timelines-chart-padding" id="settings-timelines-chart-padding" type="number" data-form-control="chartPadding"/>
+ </div>
+ <div>
+ <label for="settings-timelines-circle-radius" data-l10n-id="about-glean-metrics-table-settings-timelines-circle-radius"></label>
+ <input name="settings-timelines-circle-radius" id="settings-timelines-circle-radius" type="number" data-form-control="circleRadius"/>
+ </div>
+ <div>
+ <label for="settings-timelines-vertical-line-x-offset" data-l10n-id="about-glean-metrics-table-settings-timelines-vertical-line-x-offset"></label>
+ <input name="settings-timelines-vertical-line-x-offset" id="settings-timelines-vertical-line-x-offset" type="number" data-form-control="verticalLineXOffset"/>
+ </div>
+ <div>
+ <label for="settings-timelines-vertical-line-y-offset" data-l10n-id="about-glean-metrics-table-settings-timelines-vertical-line-y-offset"></label>
+ <input name="settings-timelines-vertical-line-y-offset" id="settings-timelines-vertical-line-y-offset" type="number" data-form-control="verticalLineYOffset"/>
+ </div>
+ </div>
+
+ <h4 data-l10n-id="about-glean-metrics-table-settings-visualization-example"></h4>
+ <div id="metrics-table-settings-timeline-example" class="chart-display"></div>
+ </div>
+ </div>
+ </div>
+ </div>
<div id="metrics-table-controls">
<div class="input">
<label for="filter-metrics" data-l10n-id="about-glean-label-for-filter-metrics"></label>
diff --git a/toolkit/content/aboutGlean.js b/toolkit/content/aboutGlean.js
@@ -207,7 +207,9 @@ function showTab(button) {
...metric,
}))
);
- updateFilteredMetricData();
+ updateFilteredMetricData(
+ document.getElementById("filter-metrics").value.toLowerCase()
+ );
updateTable();
}
}
@@ -242,7 +244,7 @@ function handleRedesign() {
document.getElementById("filter-metrics").addEventListener("input", e => {
clearTimeout(inputTimeout);
inputTimeout = setTimeout(() => {
- updateFilteredMetricData(e.target.value ?? "");
+ updateFilteredMetricData(e.target.value.toLowerCase() ?? "");
}, 200);
});
@@ -324,6 +326,19 @@ function onLoad() {
}
handleRedesign();
});
+
+ document
+ .getElementById("metrics-table-settings-button")
+ .addEventListener("click", () => {
+ const settingsDiv = document.getElementById("metrics-table-settings");
+ if (settingsDiv.getAttribute("hidden")) {
+ settingsDiv.removeAttribute("hidden");
+ } else {
+ settingsDiv.setAttribute("hidden", true);
+ }
+ settingsChanged();
+ });
+ initMetricsSettings();
}
/**
@@ -381,15 +396,18 @@ function createOrUpdateHistogram(selection, datum) {
}
selection.select("p")?.remove();
+ const chartSettings = {
+ boxPadding: 5,
+ chartMax: 150,
+ leftPadding: 20,
+ chartPadding: 50,
+ scaledMax: 110,
+ ...metricsTableSettings.histograms,
+ };
const max = values.map(d => d[1]).sort((a, b) => b - a)[0],
keyMax = values.map(d => d[0]).sort((a, b) => b - a)[0],
boxWidth = Math.max(`${Math.max(max, keyMax)}`.length * 10, 30),
- boxPadding = 5,
- chartMax = 150,
- leftPadding = 20,
- chartPadding = 50,
- scaledMax = 110,
- denom = max / scaledMax;
+ denom = max / chartSettings.scaledMax;
let hist = selection.select(`svg[data-d3-datum='${datum.fullName}']`);
if (hist.empty()) {
@@ -399,9 +417,11 @@ function createOrUpdateHistogram(selection, datum) {
.attr("data-d3-datum", datum.fullName)
.attr(
"width",
- values.length * (boxWidth + boxPadding) + chartPadding + leftPadding
+ values.length * (boxWidth + chartSettings.boxPadding) +
+ chartSettings.chartPadding +
+ chartSettings.leftPadding
)
- .attr("height", chartMax + chartPadding);
+ .attr("height", chartSettings.chartMax + chartSettings.chartPadding);
}
let boxesContainer = hist.select("g.boxes");
@@ -416,9 +436,13 @@ function createOrUpdateHistogram(selection, datum) {
newBoxes.append("text").attr("data-d3-role", "y");
newBoxes.append("text").attr("data-d3-role", "x");
- const xFn = index => boxWidth * index + boxPadding * index + leftPadding;
+ const xFn = index =>
+ boxWidth * index +
+ chartSettings.boxPadding * index +
+ chartSettings.leftPadding;
const yFn = yv =>
- Math.abs(Math.max(yv / denom, 1) - scaledMax) + (chartMax - scaledMax);
+ Math.abs(Math.max(yv / denom, 1) - chartSettings.scaledMax) +
+ (chartSettings.chartMax - chartSettings.scaledMax);
boxes
.selectAll("rect")
@@ -433,7 +457,7 @@ function createOrUpdateHistogram(selection, datum) {
boxes
.selectAll("text[data-d3-role=x]")
.attr("x", d => xFn(d[2]))
- .attr("y", chartMax + 20)
+ .attr("y", chartSettings.chartMax + 20)
.text(d => d[0]);
function focusStart(_) {
@@ -471,40 +495,72 @@ function createOrUpdateEventChart(selection, datum) {
}
selection.select("p")?.remove();
- const height = 75,
- width = 500,
- max = values.map(d => d.timestamp).sort((a, b) => b - a)[0],
- min = values.map(d => d.timestamp).sort((a, b) => a - b)[0],
- chartPadding = 50,
- circleRadius = 6;
+ const chartSettings = {
+ height: 75,
+ width: 500,
+ chartPadding: 50,
+ circleRadius: 6,
+ verticalLineXOffset: 10,
+ verticalLineYOffset: 10,
+ ...metricsTableSettings.timelines,
+ };
+ const max = values.map(d => d.timestamp).sort((a, b) => b - a)[0],
+ min = values.map(d => d.timestamp).sort((a, b) => a - b)[0];
let diagram = selection.select(`svg[data-d3-datum='${datum.fullName}']`);
if (diagram.empty()) {
diagram = selection
.append("svg")
- .classed({ timeline: true })
.attr("data-d3-datum", datum.fullName)
- .attr("width", width)
- .attr("height", height);
- diagram
- .append("line")
- .attr("x1", chartPadding)
- .attr("y1", height / 2)
- .attr("x2", width - chartPadding)
- .attr("y2", height / 2);
- diagram
+ .classed({ timeline: true });
+ }
+ diagram
+ .attr("width", chartSettings.width)
+ .attr("height", chartSettings.height);
+
+ let lineAcross = diagram.select("line[data-d3-role='across']");
+ if (lineAcross.empty()) {
+ lineAcross = diagram.append("line").attr("data-d3-role", "across");
+ }
+ lineAcross
+ .attr("x1", chartSettings.chartPadding)
+ .attr("y1", chartSettings.height / 2)
+ .attr("x2", chartSettings.width - chartSettings.chartPadding)
+ .attr("y2", chartSettings.height / 2);
+
+ let leftLineThrough = diagram.select("line[data-d3-role='left-through']");
+ if (leftLineThrough.empty()) {
+ leftLineThrough = diagram
.append("line")
- .attr("x1", chartPadding + 10)
- .attr("y1", height / 2 - 10)
- .attr("x2", chartPadding + 10)
- .attr("y2", height / 2 + 10);
- diagram
+ .attr("data-d3-role", "left-through");
+ }
+ leftLineThrough
+ .attr("x1", chartSettings.chartPadding + chartSettings.verticalLineXOffset)
+ .attr("y1", chartSettings.height / 2 - chartSettings.verticalLineYOffset)
+ .attr("x2", chartSettings.chartPadding + chartSettings.verticalLineXOffset)
+ .attr("y2", chartSettings.height / 2 + chartSettings.verticalLineYOffset);
+
+ let rightLineThrough = diagram.select("line[data-d3-role='right-through']");
+ if (rightLineThrough.empty()) {
+ rightLineThrough = diagram
.append("line")
- .attr("x1", width - chartPadding - 10)
- .attr("y1", height / 2 - 10)
- .attr("x2", width - chartPadding - 10)
- .attr("y2", height / 2 + 10);
+ .attr("data-d3-role", "right-through");
}
+ rightLineThrough
+ .attr(
+ "x1",
+ chartSettings.width -
+ chartSettings.chartPadding -
+ chartSettings.verticalLineXOffset
+ )
+ .attr("y1", chartSettings.height / 2 - chartSettings.verticalLineYOffset)
+ .attr(
+ "x2",
+ chartSettings.width -
+ chartSettings.chartPadding -
+ chartSettings.verticalLineXOffset
+ )
+ .attr("y2", chartSettings.height / 2 + chartSettings.verticalLineYOffset);
let code = selection.select("pre");
if (code.empty()) {
@@ -516,7 +572,12 @@ function createOrUpdateEventChart(selection, datum) {
const xFn = d3.scale
.linear()
.domain([min, max])
- .range([10 + chartPadding, width - chartPadding - 10]);
+ .range([
+ chartSettings.verticalLineXOffset + chartSettings.chartPadding,
+ chartSettings.width -
+ chartSettings.chartPadding -
+ chartSettings.verticalLineXOffset,
+ ]);
let eventsContainer = diagram.select("g.events");
if (eventsContainer.empty()) {
@@ -533,10 +594,7 @@ function createOrUpdateEventChart(selection, datum) {
.classed({ event: true })
.attr("tabindex", 0);
- newEvents
- .append("circle")
- .attr("cy", height / 2)
- .attr("r", circleRadius);
+ newEvents.append("circle");
function focusStart(_) {
this.classList.add("hovered");
@@ -559,7 +617,7 @@ function createOrUpdateEventChart(selection, datum) {
const text = this.appendChild(
document.createElementNS("http://www.w3.org/2000/svg", "text")
);
- text.setAttribute("y", height / 2 + 25);
+ text.setAttribute("y", chartSettings.height / 2 + 25);
text.setAttribute(
"x",
xFn(dataPoint.timestamp) - `${dataPoint.timestamp}`.length * 4.5
@@ -569,7 +627,11 @@ function createOrUpdateEventChart(selection, datum) {
events.attr("data-d3-datum", d => `${d.fullName}-${d.index}-${d.timestamp}`);
- events.selectAll("circle").attr("cx", d => xFn(d.timestamp));
+ events
+ .selectAll("circle")
+ .attr("cy", chartSettings.height / 2)
+ .attr("cx", d => xFn(d.timestamp))
+ .attr("r", chartSettings.circleRadius);
events
.on("focusin", select)
@@ -580,6 +642,152 @@ function createOrUpdateEventChart(selection, datum) {
events.exit().remove();
}
+const METRICS_TABLE_SETTINGS_KEY = "about-glean-metrics-table-settings";
+/**
+ * When adding new fields to the metrics table settings,
+ * a corresponding element matching the query selector
+ * `[data-form-control=<key>]` must be present in the DOM.
+ */
+const metricsTableSettings = {
+ hideEmptyValueRows: false,
+ histograms: {
+ boxPadding: 5,
+ chartMax: 150,
+ leftPadding: 20,
+ chartPadding: 50,
+ scaledMax: 110,
+ },
+ timelines: {
+ height: 75,
+ width: 500,
+ chartPadding: 50,
+ circleRadius: 6,
+ verticalLineXOffset: 10,
+ verticalLineYOffset: 10,
+ },
+};
+
+function initMetricsSettings() {
+ const handleSetting = (obj, key, parent) => {
+ let element = parent.querySelector(`[data-form-control='${key}']`);
+ let valueFn = e => e.target.value;
+ if (!element && typeof obj[key] !== "object") {
+ console.error(
+ new Error(
+ `Unable to find form control with key '${key}' in the parent element`
+ ),
+ parent
+ );
+ return;
+ }
+ switch (typeof obj[key]) {
+ case "boolean":
+ valueFn = e => e.target.checked;
+ obj[key] = valueFn({ target: element });
+ break;
+ case "object":
+ element = parent.querySelector(`[data-form-group='${key}']`);
+ if (!element) {
+ console.error(
+ new Error(
+ `Unable to find form control with key '${key}' in the parent element`
+ ),
+ parent
+ );
+ return;
+ }
+ for (const subKey of Object.keys(obj[key])) {
+ handleSetting(obj[key], subKey, element);
+ }
+ break;
+ case "number":
+ valueFn = e => parseInt(e.target.value);
+ // eslint-disable-next-line no-fallthrough
+ default:
+ if (element.type !== typeof obj[key]) {
+ console.warn(
+ new Error(
+ `Form control input type does not match JavaScript value type ${typeof obj[key]}`
+ )
+ );
+ }
+ if (valueFn({ target: element })) {
+ obj[key] = valueFn({ target: element });
+ } else {
+ element.value = obj[key];
+ }
+ }
+ element.addEventListener("input", handleSettingChange(obj, valueFn));
+ };
+
+ for (const key of Object.keys(metricsTableSettings)) {
+ handleSetting(
+ metricsTableSettings,
+ key,
+ document.getElementById("metrics-table-settings")
+ );
+ }
+}
+
+function handleSettingChange(obj, valueFn) {
+ return e => {
+ obj[e.target.getAttribute("data-form-control")] = valueFn(e);
+ settingsChanged();
+ };
+}
+
+function settingsChanged() {
+ createOrUpdateHistogram(
+ d3.select("#metrics-table-settings-histogram-example"),
+ {
+ fullName: "histogram-example",
+ value: {
+ values: {
+ 0: 1,
+ 1: 5,
+ 2: 4,
+ 3: 0,
+ },
+ },
+ }
+ );
+
+ createOrUpdateEventChart(
+ d3.select("#metrics-table-settings-timeline-example"),
+ {
+ fullName: "timeline-example",
+ value: [
+ {
+ timestamp: 0,
+ extra: {
+ value: 1,
+ },
+ },
+ {
+ timestamp: 1,
+ extra: {
+ value: 2,
+ },
+ },
+ {
+ timestamp: 4,
+ extra: {
+ value: 3,
+ },
+ },
+ {
+ timestamp: 8,
+ extra: {
+ value: 4,
+ },
+ },
+ ],
+ }
+ );
+
+ updateTable();
+}
+
function updateValueSelection(selection) {
// Set the `data-l10n-id` attribute to the appropriate warning if the value is invalid, otherwise
// unset it by returning `null`.
@@ -686,7 +894,15 @@ function prettyPrint(jsonValue) {
function updateTable() {
LIMITED_METRIC_DATA = FILTERED_METRIC_DATA.toSorted((a, b) =>
d3.ascending(a.fullName, b.fullName)
- ).filter((_, i) => i >= LIMIT_OFFSET && i < LIMIT_COUNT + LIMIT_OFFSET);
+ )
+ // Filter out rows whose datum elements have either a) been loaded and have a value, or b) have not yet been loaded.
+ .filter(d =>
+ metricsTableSettings.hideEmptyValueRows
+ ? (d.value !== undefined && d.value !== null) || !d.loaded
+ : true
+ )
+ // Filter down to only the datum elements whose indexes fall in the limit offset+count.
+ .filter((_, i) => i >= LIMIT_OFFSET && i < LIMIT_COUNT + LIMIT_OFFSET);
// Let's talk about d3.js
// `d3.select` is a rough equivalent to `document.querySelector`, but the resulting object(s) are things d3 knows how to manipulate.
@@ -813,9 +1029,9 @@ function updateFilteredMetricData(searchString) {
};
FILTERED_METRIC_DATA = MAPPED_METRIC_DATA.filter(
datum =>
- datum.category.includes(searchString) ||
- datum.name.includes(searchString) ||
- datum.type.includes(searchString) ||
+ datum.category.toLowerCase().includes(searchString) ||
+ datum.name.toLowerCase().includes(searchString) ||
+ datum.type.toLowerCase().includes(searchString) ||
simpleTypeValueSearch(datum)
);
}
diff --git a/toolkit/content/tests/browser/browser_about_glean.js b/toolkit/content/tests/browser/browser_about_glean.js
@@ -447,3 +447,91 @@ add_task(async function test_about_glean_ping_groups_and_none_label() {
});
});
});
+
+add_task(async function test_about_glean_metrics_table_settings() {
+ Services.fog.testResetFOG();
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("about.glean.redesign.enabled");
+ });
+ Services.prefs.setBoolPref("about.glean.redesign.enabled", true);
+
+ await BrowserTestUtils.withNewTab("about:glean", async browser => {
+ await ContentTask.spawn(browser, null, async function () {
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+ const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+ );
+
+ content.document.getElementById("category-metrics-table").click();
+
+ let tableBody;
+ const fetchTableBody = () => {
+ tableBody = content.document.getElementById("metrics-table-body");
+ };
+ fetchTableBody();
+ let currentFirstChild =
+ tableBody.children[0].attributes["data-d3-row"].value;
+
+ const tableFirstChildChanged = () => {
+ fetchTableBody();
+ if (
+ currentFirstChild !=
+ tableBody.children[0].attributes["data-d3-row"].value
+ ) {
+ currentFirstChild =
+ tableBody.children[0].attributes["data-d3-row"].value;
+ return true;
+ }
+ return false;
+ };
+
+ const input = content.document.getElementById("filter-metrics");
+ input.value = "aBool";
+ input.dispatchEvent(new Event("input"));
+
+ await TestUtils.waitForCondition(
+ tableFirstChildChanged,
+ "Wait for the table's first child to change",
+ 100,
+ 3
+ );
+
+ content.document
+ .querySelector(
+ "[data-d3-row='testOnlyIpc.aBool'] button[data-l10n-id='about-glean-button-load-value']"
+ )
+ .click();
+
+ content.document.getElementById("metrics-table-settings-button").click();
+
+ const checkbox = content.document.getElementById(
+ "settings-hide-empty-value-rows"
+ );
+ checkbox.checked = true;
+ checkbox.dispatchEvent(new Event("input"));
+
+ const tableNoChildren = () => {
+ fetchTableBody();
+ if (tableBody.children.length === 0) {
+ return true;
+ }
+ return false;
+ };
+
+ await TestUtils.waitForCondition(
+ tableNoChildren,
+ "Wait for the table's children to be empty",
+ 100,
+ 3
+ );
+
+ Assert.equal(
+ content.document.querySelector("[data-d3-row='testOnlyIpc.aBool']"),
+ null,
+ "Data row for `testOnlyIpc.aBool` should not exist"
+ );
+ });
+ });
+});
diff --git a/toolkit/locales/en-US/toolkit/about/aboutGlean.ftl b/toolkit/locales/en-US/toolkit/about/aboutGlean.ftl
@@ -169,6 +169,36 @@ about-glean-metrics-table-header-type = Type
about-glean-metrics-table-header-value = Value
# This message refers to the UI action buttons for a given metric.
about-glean-metrics-table-header-actions = Actions
+about-glean-metrics-table-settings-button = Settings
+
+# Settings for the metrics table and its visualizations in about:glean
+about-glean-metrics-table-settings-title = Metrics Table Settings
+about-glean-metrics-table-settings-category-general = General
+about-glean-metrics-table-settings-hide-empty-value-rows = Hide empty value rows
+
+about-glean-metrics-table-settings-category-visualizations = Visualizations
+# This is a heading that is immediately followed by an example data visualization
+about-glean-metrics-table-settings-visualization-example = Example
+
+about-glean-metrics-table-settings-category-visualizations-histogram = Histogram
+about-glean-metrics-table-settings-histograms-chart-max = Chart maximum height
+# The maximum height after to which the y-values on the chart will be scaled
+about-glean-metrics-table-settings-histograms-scaled-max = Scaled maximum height
+about-glean-metrics-table-settings-histograms-box-padding = Box padding
+about-glean-metrics-table-settings-histograms-chart-padding = Chart padding
+about-glean-metrics-table-settings-histograms-left-padding = Additional left padding
+
+about-glean-metrics-table-settings-category-visualizations-timeline = Timeline
+about-glean-metrics-table-settings-timelines-height = Height
+about-glean-metrics-table-settings-timelines-width = Width
+about-glean-metrics-table-settings-timelines-chart-padding = Chart padding
+# The radius of each circle denoting individual events recorded for an event metric
+about-glean-metrics-table-settings-timelines-circle-radius = Circle radius
+# The offset on the x-axis from the end of the horizontal line for the y-axis line
+about-glean-metrics-table-settings-timelines-vertical-line-x-offset = Y-axis X offset
+# The offset on the y-axis from the x-axis for the y-axis line
+about-glean-metrics-table-settings-timelines-vertical-line-y-offset = Y-axis Y offset
+
# Label displayed near an input field that can be used to filter metrics
about-glean-label-for-filter-metrics = Filter