fix(security): render visualizer data as text (#11186)

Signed-off-by: Jasper Hugo <jasper@spiral.xyz>
This commit is contained in:
Jasper
2026-08-13 02:43:08 +00:00
committed by GitHub
parent 589ac048e0
commit 9b70cc4774
6 changed files with 126 additions and 17 deletions
@@ -1720,6 +1720,64 @@ mod tests {
assert!(result.is_ok());
assert_mcp_apps_result(&result.unwrap(), "ui://autovisualiser/mermaid", "mermaid");
}
#[test]
fn donut_legend_renders_dynamic_values_as_text() {
let html = AutoVisualiserRouter::new()
.get_template_html("ui://autovisualiser/donut")
.unwrap();
assert!(!html.contains("legendEl.innerHTML"));
assert!(html.contains("labelEl.textContent = label"));
assert!(html.contains("valueEl.textContent = pct"));
}
#[test]
fn chord_tooltips_render_dynamic_values_as_text() {
let html = AutoVisualiserRouter::new()
.get_template_html("ui://autovisualiser/chord")
.unwrap();
assert!(!html.contains(".html("));
assert!(html.contains(".text(line)"));
assert!(html.contains("data.labels[d.source.index]"));
assert!(html.contains("data.labels[d.target.index]"));
}
#[test]
fn mermaid_errors_render_as_text_without_changing_svg_rendering() {
let html = AutoVisualiserRouter::new()
.get_template_html("ui://autovisualiser/mermaid")
.unwrap();
assert!(html.contains("errorEl.textContent = String(err.message || err)"));
assert!(html.contains("output.replaceChildren(errorEl)"));
assert!(html.contains("output.innerHTML = result.svg"));
assert!(!html.contains("'<div class=\"mermaid-error\">' +"));
}
#[test]
fn sankey_tooltips_render_dynamic_values_as_text() {
let html = AutoVisualiserRouter::new()
.get_template_html("ui://autovisualiser/sankey")
.unwrap();
assert!(!html.contains(".html("));
assert!(html.contains("tooltip.append(index === 0 ? \"strong\" : \"span\").text(line)"));
assert!(html.contains("d.source.name + \"\" + d.target.name"));
assert!(html.contains("var lines = [d.name]"));
}
#[test]
fn treemap_tooltips_render_dynamic_values_as_text() {
let html = AutoVisualiserRouter::new()
.get_template_html("ui://autovisualiser/treemap")
.unwrap();
assert!(!html.contains("tt.innerHTML"));
assert!(html.contains("name.textContent = d.data.name"));
assert!(html.contains("document.createTextNode(d.data.category)"));
}
}
#[cfg(test)]
@@ -87,8 +87,11 @@
.attr("d", arcGen)
.on("mouseover", function (event, d) {
fadeChords(g, d.index, 0.08);
tooltipEl.style("opacity", 1)
.html("<strong>" + data.labels[d.index] + "</strong><br/>Total: " + d.value.toLocaleString());
setTooltip([
data.labels[d.index],
"Total: " + d.value.toLocaleString(),
], true);
tooltipEl.style("opacity", 1);
McpAppBridge.positionTooltip(tooltipEl, event);
})
.on("mouseout", function () {
@@ -114,9 +117,11 @@
d3.select(this).style("fill-opacity", 0.8);
var s2t = data.matrix[d.source.index][d.target.index];
var t2s = data.matrix[d.target.index][d.source.index];
tooltipEl.style("opacity", 1)
.html(data.labels[d.source.index] + " → " + data.labels[d.target.index] + ": " + s2t.toLocaleString() +
"<br/>" + data.labels[d.target.index] + " → " + data.labels[d.source.index] + ": " + t2s.toLocaleString());
setTooltip([
data.labels[d.source.index] + " → " + data.labels[d.target.index] + ": " + s2t.toLocaleString(),
data.labels[d.target.index] + " → " + data.labels[d.source.index] + ": " + t2s.toLocaleString(),
], false);
tooltipEl.style("opacity", 1);
McpAppBridge.positionTooltip(tooltipEl, event);
})
.on("mouseout", function () {
@@ -125,6 +130,14 @@
});
}
function setTooltip(lines, boldFirst) {
tooltipEl.selectAll("*").remove();
lines.forEach(function (line, index) {
if (index > 0) tooltipEl.append("br");
tooltipEl.append(index === 0 && boldFirst ? "strong" : "span").text(line);
});
}
function fadeChords(g, focusIndex, opacity) {
g.selectAll(".chord-ribbon").style("opacity", function (d) {
if (focusIndex === null) return 1;
@@ -134,9 +134,25 @@
var total = cd.values.reduce(function (a, b) { return a + b; }, 0);
cd.labels.forEach(function (label, i) {
var pct = total > 0 ? ((cd.values[i] / total) * 100).toFixed(1) : "0";
legendEl.innerHTML += '<div class="legend-item"><div class="legend-dot" style="background:' + cd.colors[i] + '"></div>' +
'<span class="legend-label">' + label + '</span>' +
'<span class="legend-value">' + pct + '%</span></div>';
var item = document.createElement("div");
item.className = "legend-item";
var dot = document.createElement("div");
dot.className = "legend-dot";
dot.style.background = cd.colors[i];
item.appendChild(dot);
var labelEl = document.createElement("span");
labelEl.className = "legend-label";
labelEl.textContent = label;
item.appendChild(labelEl);
var valueEl = document.createElement("span");
valueEl.className = "legend-value";
valueEl.textContent = pct + "%";
item.appendChild(valueEl);
legendEl.appendChild(item);
});
card.appendChild(legendEl);
grid.appendChild(card);
@@ -135,7 +135,10 @@
}).catch(function (err) {
// Remove any error artefacts mermaid injected (bomb-icon SVGs, etc.)
document.querySelectorAll('[id^="d' + uid + '"]').forEach(function (el) { el.remove(); });
output.innerHTML = '<div class="mermaid-error">' + (err.message || err) + "</div>";
var errorEl = document.createElement("div");
errorEl.className = "mermaid-error";
errorEl.textContent = String(err.message || err);
output.replaceChildren(errorEl);
setTimeout(McpAppBridge.reportSize, 80);
});
}
@@ -172,8 +172,11 @@
.attr("stroke", function (d) { return nodeColor(d.source); })
.attr("stroke-width", function (d) { return Math.max(1, d.width); })
.on("mouseover", function (event, d) {
tooltip.style("opacity", 1)
.html("<strong>" + d.source.name + " → " + d.target.name + "</strong><br/>" + d.value.toLocaleString());
setTooltip([
d.source.name + " → " + d.target.name,
d.value.toLocaleString(),
]);
tooltip.style("opacity", 1);
McpAppBridge.positionTooltip(tooltip, event);
})
.on("mouseout", function () { tooltip.style("opacity", 0); });
@@ -188,10 +191,11 @@
.on("mouseover", function (event, d) {
var totalIn = d.targetLinks.reduce(function (s, l) { return s + l.value; }, 0);
var totalOut = d.sourceLinks.reduce(function (s, l) { return s + l.value; }, 0);
tooltip.style("opacity", 1)
.html("<strong>" + d.name + "</strong>" +
(totalIn ? "<br/>In: " + totalIn.toLocaleString() : "") +
(totalOut ? "<br/>Out: " + totalOut.toLocaleString() : ""));
var lines = [d.name];
if (totalIn) lines.push("In: " + totalIn.toLocaleString());
if (totalOut) lines.push("Out: " + totalOut.toLocaleString());
setTooltip(lines);
tooltip.style("opacity", 1);
McpAppBridge.positionTooltip(tooltip, event);
})
.on("mouseout", function () { tooltip.style("opacity", 0); });
@@ -205,6 +209,14 @@
.text(function (d) { return d.name; });
}
function setTooltip(lines) {
tooltip.selectAll("*").remove();
lines.forEach(function (line, index) {
if (index > 0) tooltip.append("br");
tooltip.append(index === 0 ? "strong" : "span").text(line);
});
}
function renderData(data) {
document.getElementById("loadingIndicator").classList.add("hidden");
initializeSVG();
@@ -105,8 +105,15 @@
.on("mouseover", function (event, d) {
var pct = ((d.data.value / root.value) * 100).toFixed(1);
var tt = document.getElementById("tooltip");
tt.innerHTML = "<strong>" + d.data.name + "</strong><br/>" + formatValue(d.data.value) + " (" + pct + "%)" +
(d.data.category ? "<br/>" + d.data.category : "");
var name = document.createElement("strong");
name.textContent = d.data.name;
tt.replaceChildren(name);
tt.appendChild(document.createElement("br"));
tt.appendChild(document.createTextNode(formatValue(d.data.value) + " (" + pct + "%)"));
if (d.data.category) {
tt.appendChild(document.createElement("br"));
tt.appendChild(document.createTextNode(d.data.category));
}
tt.style.opacity = 1;
McpAppBridge.positionTooltip(tt, event);
})