fix(security): render visualizer data as text (#11186)
Signed-off-by: Jasper Hugo <jasper@spiral.xyz>
This commit is contained in:
@@ -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);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user