diff --git a/.github/workflows/deploy_dashboard.yml b/.github/workflows/deploy_dashboard.yml index 7ca01adc..454aaa00 100644 --- a/.github/workflows/deploy_dashboard.yml +++ b/.github/workflows/deploy_dashboard.yml @@ -37,6 +37,8 @@ jobs: echo "Syncing latest interop logs from SDK rolling releases..." # Download Python Metrics directly into the itk-dashboard directory curl -L -f -o itk-dashboard/itk_python.json https://github.com/a2aproject/a2a-python/releases/download/nightly-metrics/itk_python.json || echo "{}" > itk-dashboard/itk_python.json + # Download Go Metrics directly into the itk-dashboard directory + curl -L -f -o itk-dashboard/itk_go.json https://github.com/a2aproject/a2a-go/releases/download/nightly-metrics/itk_go.json || echo "{}" > itk-dashboard/itk_go.json echo "Metrics synchronized successfully!" - name: Prepare static site structure diff --git a/itk-dashboard/app.js b/itk-dashboard/app.js index 11067da8..dbec5bad 100644 --- a/itk-dashboard/app.js +++ b/itk-dashboard/app.js @@ -2,13 +2,12 @@ document.addEventListener("DOMContentLoaded", () => { const sdkTabs = document.getElementById("sdk-tabs"); const loadingState = document.getElementById("loading-state"); const dashboardBody = document.getElementById("dashboard-body"); - const lastRunTime = document.getElementById("last-run-time"); - const lastCommit = document.getElementById("last-commit"); const historyCount = document.getElementById("history-count"); const historyTimelineList = document.getElementById( "history-timeline-list" ); - const topologiesList = document.getElementById("topologies-list"); + const summaryContainer = document.getElementById("summary-container"); + const pairwiseList = document.getElementById("pairwise-list"); // List of protocols and behaviors to draw the matrix grid const PROTOCOLS = ["jsonrpc", "grpc", "http_json"]; @@ -42,6 +41,7 @@ document.addEventListener("DOMContentLoaded", () => { let activeSDK = "python"; let historyData = []; let activeRunIndex = 0; + let maxTopologySdks = []; // Dynamic list of all SDK nodes in the full parent topology // Initial setup: add event listeners to tabs sdkTabs.querySelectorAll(".tab-btn").forEach((btn) => { @@ -57,6 +57,24 @@ document.addEventListener("DOMContentLoaded", () => { }); }); + // Dynamically track max SDKs from the current run for helper calculations + function initializeMaxTopologySdks(run) { + const scenarios = run.scenarios || []; + let maxSDKs = 0; + maxTopologySdks = []; + scenarios.forEach((scenario) => { + const sdks = scenario.sdks || []; + if (sdks.length > maxSDKs) { + maxSDKs = sdks.length; + maxTopologySdks = [...sdks]; + } + }); + console.log( + `[ITK-DASHBOARD] Dynamically derived parent SDKs list:`, + maxTopologySdks + ); + } + // Load data by URL natively (CORS-free served locally) async function loadDashboardData(url) { console.log( @@ -110,6 +128,7 @@ document.addEventListener("DOMContentLoaded", () => { ); activeRunIndex = 0; + initializeMaxTopologySdks(historyData[0]); renderDashboard(); console.log(`[ITK-DASHBOARD] Dashboard populated successfully!`); } catch (err) { @@ -172,6 +191,7 @@ document.addEventListener("DOMContentLoaded", () => { `; item.addEventListener("click", () => { activeRunIndex = index; + initializeMaxTopologySdks(historyData[index]); // Re-render active run context document .querySelectorAll(".timeline-item") @@ -186,161 +206,324 @@ document.addEventListener("DOMContentLoaded", () => { renderActiveRun(); } - // Render currently selected run + // Render currently selected run statically (Summary Star Topology at top, Pairwise at bottom) function renderActiveRun() { const run = historyData[activeRunIndex]; if (!run) return; - // Update Header Meta - const runDate = new Date(run.timestamp); - lastRunTime.textContent = runDate.toLocaleString(); - lastCommit.innerHTML = `${run.commit_sha.substring(0, 7)}`; + summaryContainer.innerHTML = ""; + pairwiseList.innerHTML = ""; - topologiesList.innerHTML = ""; - - // Process and render each scenario / topology card const scenarios = run.scenarios || []; if (scenarios.length === 0) { - topologiesList.innerHTML = + summaryContainer.innerHTML = '

No test scenarios executed in this run.

'; return; } - // Group scenarios by SDK list - const groupedTopologies = {}; + // 1. Identify the top-level (maximum node count) topology key and its matching scenarios + let maxNodes = 0; + let summaryTopologyKey = ""; scenarios.forEach((scenario) => { - const sdkKey = (scenario.sdks || []).join(","); - if (!groupedTopologies[sdkKey]) { - groupedTopologies[sdkKey] = { - sdks: scenario.sdks || [], - edges: scenario.edges || [], - traversal: scenario.traversal || "euler", - scenarios: [], - }; + const size = (scenario.sdks || []).length; + if (size > maxNodes) { + maxNodes = size; + summaryTopologyKey = (scenario.sdks || []).join(","); } - groupedTopologies[sdkKey].scenarios.push(scenario); }); - // Render each grouped topology card - Object.keys(groupedTopologies).forEach((sdkKey, idx) => { - const group = groupedTopologies[sdkKey]; - const card = document.createElement("div"); - card.className = "panel glass topology-card"; - - const cardId = `topology-card-${idx}`; - const svgId = `svg-canvas-${idx}`; - - // Create clean dynamic name based on list of SDKs - const topologyName = `Star Topology (${group.sdks.length} Nodes)`; - - card.innerHTML = ` -
- -
- - - - - - - - - -
+ const summarySdks = summaryTopologyKey + ? summaryTopologyKey.split(",") + : []; + + if (summaryTopologyKey) { + const summaryScenarios = scenarios.filter( + (s) => (s.sdks || []).join(",") === summaryTopologyKey + ); + + if (summaryScenarios.length > 0) { + const edges = summaryScenarios[0].edges || []; + const card = document.createElement("div"); + card.className = "panel glass topology-card"; + + const cardId = "topology-card-summary"; + const svgId = "svg-canvas-summary"; - -
- - - - - ${BEHAVIORS.map((b) => ``).join("")} - - - - - -
Protocol${b.label}
+ const commitLink = `${run.commit_sha.substring(0, 7)}`; + const runDateStr = new Date(run.timestamp).toLocaleString(); + + card.innerHTML = ` +
+
Nightly Run: ${runDateStr}
+
Commit SHA: ${commitLink}
-
- `; +
+ +
+ + + + + + + + + + +
+ + +
+ + + + + ${PROTOCOLS.map((p) => ``).join("")} + + + + + +
Behavior / Feature${p}
+
+
+ `; + + summaryContainer.appendChild(card); - topologiesList.appendChild(card); + // Draw node graph and render fully filled behavior rows vs protocol columns compatibility matrix + drawTopologyGraph(svgId, summarySdks, edges); + renderGroupMatrix(`matrix-body-${svgId}`, summaryScenarios); + } + } else { + summaryContainer.innerHTML = + '

No summary scenario found.

'; + } - // 1. Draw topology graph SVG - drawTopologyGraph(svgId, group.sdks, group.edges, topologyName); + // 2. Identify all unique pairwise (exactly 2 nodes) topology keys and render their fully-filled grids + const uniquePairwiseKeys = []; + scenarios.forEach((scenario) => { + const sdks = scenario.sdks || []; + if (sdks.length === 2) { + const key = sdks.join(","); + if (!uniquePairwiseKeys.includes(key)) { + uniquePairwiseKeys.push(key); + } + } + }); - // 2. Render the consolidated matrix content for this group - renderGroupMatrix(`matrix-body-${idx}`, group.scenarios); + // Sort pairwise keys deterministically based on the order of SDKs in the summary visualization + uniquePairwiseKeys.sort((keyA, keyB) => { + const getRank = (key) => { + const sdks = key.split(","); + const ranks = sdks.map((s) => { + const idx = summarySdks.findIndex( + (sumSdk) => sumSdk.toLowerCase() === s.toLowerCase() + ); + return idx !== -1 ? idx : 999; + }); + ranks.sort((a, b) => a - b); + return ranks; + }; + const rankA = getRank(keyA); + const rankB = getRank(keyB); + if (rankA[0] !== rankB[0]) { + return rankA[0] - rankB[0]; + } + return rankA[1] - rankB[1]; }); + + if (uniquePairwiseKeys.length > 0) { + uniquePairwiseKeys.forEach((key, idx) => { + const pairwiseSdks = key.split(","); + const pairwiseScenarios = scenarios.filter( + (s) => (s.sdks || []).join(",") === key + ); + + if (pairwiseScenarios.length > 0) { + const edges = pairwiseScenarios[0].edges || []; + const card = document.createElement("div"); + card.className = "panel glass topology-card"; + + const cardId = `topology-card-pairwise-${idx}`; + const svgId = `svg-canvas-pairwise-${idx}`; + + card.innerHTML = ` +
+ +
+ + + + + + + + + + +
+ + +
+ + + + + ${PROTOCOLS.map((p) => ``).join("")} + + + + + +
Behavior / Feature${p}
+
+
+ `; + + pairwiseList.appendChild(card); + + drawTopologyGraph(svgId, pairwiseSdks, edges); + renderGroupMatrix( + `matrix-body-${svgId}`, + pairwiseScenarios + ); + } + }); + } else { + pairwiseList.innerHTML = + '

No pairwise scenarios executed in this run.

'; + } + } + + // Helper: Format SDK string into pretty display names + function formatSdkName(sdk) { + if (!sdk) return ""; + if (sdk.toLowerCase() === "current") return "Current"; + + const parts = sdk.split("_"); + let lang = parts[0]; + if (lang.toLowerCase() === "dotnet") { + lang = ".NET"; + } else { + lang = lang.charAt(0).toUpperCase() + lang.slice(1); + } + + if (parts.length > 1) { + let ver = parts[1]; + if (ver.startsWith("v") && ver.length === 3) { + ver = `v${ver[1]}.${ver[2]}`; + } + return `${lang} ${ver}`; + } + return lang; + } + + // Helper: Get premium styling colors for a given language/SDK string + function getSdkColors(sdk) { + const str = (sdk || "").toLowerCase(); + if (str === "current") { + return { fill: "#1e3a8a", stroke: "#3b82f6", text: "#eff6ff" }; + } + if (str.startsWith("python")) { + return { fill: "#422006", stroke: "#eab308", text: "#fef08a" }; // Yellow Gold + } + if (str.startsWith("go")) { + return { fill: "#083344", stroke: "#06b6d4", text: "#cffafe" }; // Cyan + } + if (str.startsWith("java")) { + return { fill: "#450a0a", stroke: "#ef4444", text: "#fee2e2" }; // Red/Orange + } + if (str.startsWith("dotnet")) { + return { fill: "#2e1065", stroke: "#a855f7", text: "#f3e8ff" }; // Purple + } + return { fill: "#1e293b", stroke: "#64748b", text: "#f8fafc" }; // Slate Default + } + + // Helper: Calculate exact intersection of line segment with target node rectangle border + function getBorderIntersection(fromPos, toPos) { + const dx = toPos.x - fromPos.x; + const dy = toPos.y - fromPos.y; + if (dx === 0 && dy === 0) return { x: fromPos.x, y: fromPos.y }; + + const w = 55; // half width of pill rectangle + const h = 14; // half height of pill rectangle + + const tx = dx !== 0 ? w / Math.abs(dx) : Infinity; + const ty = dy !== 0 ? h / Math.abs(dy) : Infinity; + const t = Math.min(tx, ty); + + return { + x: fromPos.x + t * dx, + y: fromPos.y + t * dy, + }; } - // Helper: Render SVG Topology Node-Link diagram - function drawTopologyGraph(svgId, sdks, edges, scenarioName) { + // Helper: Render SVG Topology Node-Link diagram in a gorgeous tree layout + function drawTopologyGraph(svgId, sdks, edges) { const svg = document.getElementById(svgId); if (!svg) return; - const width = 400; + // End and start markers are statically pre-configured in the SVG defs template + const height = 300; - const centerX = width / 2; - const centerY = height / 2; const numNodes = sdks.length; + // Determine root index (Current is usually index 0) + let rootIdx = sdks.findIndex((s) => s.toLowerCase() === "current"); + if (rootIdx === -1) rootIdx = 0; + const nodePositions = {}; - // Determine Layout strategy - const isStar = - scenarioName.toLowerCase().includes("star") || numNodes > 3; - const isChain = - scenarioName.toLowerCase().includes("chain") || - scenarioName.toLowerCase().includes("linear"); - - if (isStar) { - // Node 0 in center, others arranged in circle - nodePositions[0] = { x: centerX, y: centerY, isCenter: true }; - const radius = 115; - for (let i = 1; i < numNodes; i++) { - const angle = - ((i - 1) / (numNodes - 1)) * 2 * Math.PI - Math.PI / 2; - nodePositions[i] = { - x: centerX + radius * Math.cos(angle), - y: centerY + radius * Math.sin(angle), - isCenter: false, - }; - } - } else if (isChain && numNodes > 1) { - // Arrange horizontally in a line - const startX = 60; - const spacing = (width - 2 * startX) / (numNodes - 1); - for (let i = 0; i < numNodes; i++) { - nodePositions[i] = { - x: startX + i * spacing, - y: centerY, - isCenter: false, - }; - } - } else { - // Standard circular layout - const radius = 110; - for (let i = 0; i < numNodes; i++) { - const angle = (i / numNodes) * 2 * Math.PI - Math.PI / 2; - nodePositions[i] = { - x: centerX + radius * Math.cos(angle), - y: centerY + radius * Math.sin(angle), - isCenter: false, + // Root positioned on the left + nodePositions[rootIdx] = { x: 80, y: height / 2, isRoot: true }; + + // Other nodes spreading to the right in a clean tree hierarchy + const childrenIndices = []; + for (let i = 0; i < numNodes; i++) { + if (i !== rootIdx) childrenIndices.push(i); + } + + const numChildren = childrenIndices.length; + if (numChildren === 1) { + nodePositions[childrenIndices[0]] = { + x: 320, + y: height / 2, + isRoot: false, + }; + } else if (numChildren > 1) { + const startY = 50; + const endY = height - 50; + const spacing = (endY - startY) / (numChildren - 1); + childrenIndices.forEach((childIdx, i) => { + nodePositions[childIdx] = { + x: 320, + y: startY + i * spacing, + isRoot: false, }; - } + }); } - // 1. Draw edges/links + // 1. Draw single connecting lines strictly between the dynamic inner rectangle borders of connected nodes + const drawnPairs = new Set(); edges.forEach((edge) => { const parts = edge.split("->"); if (parts.length !== 2) return; const fromIdx = parseInt(parts[0], 10); const toIdx = parseInt(parts[1], 10); + if (fromIdx === toIdx) return; + + const pairKey = + Math.min(fromIdx, toIdx) + "-" + Math.max(fromIdx, toIdx); + if (drawnPairs.has(pairKey)) return; + drawnPairs.add(pairKey); + const fromNode = nodePositions[fromIdx]; const toNode = nodePositions[toIdx]; @@ -349,27 +532,100 @@ document.addEventListener("DOMContentLoaded", () => { "http://www.w3.org/2000/svg", "line" ); - line.setAttribute("x1", fromNode.x); - line.setAttribute("y1", fromNode.y); - line.setAttribute("x2", toNode.x); - line.setAttribute("y2", toNode.y); + + // Dynamically calculate precise border intersection points for any arbitrary connection angle + const startPt = getBorderIntersection(fromNode, toNode); + const endPt = getBorderIntersection(toNode, fromNode); + + line.setAttribute("x1", startPt.x); + line.setAttribute("y1", startPt.y); + line.setAttribute("x2", endPt.x); + line.setAttribute("y2", endPt.y); line.setAttribute("class", "svg-edge"); - line.setAttribute( - "marker-end", - `url(#arrow-${svgId.split("-").pop()})` - ); - // Style bi-directional links subtly different + // Check if reverse edge exists to show bi-directional arrows pointing both ways const reverseEdge = `${toIdx}->${fromIdx}`; - if (edges.includes(reverseEdge)) { - line.setAttribute("class", "svg-edge bi-directional"); + const isBidirectional = edges.includes(reverseEdge); + + if (isBidirectional) { + line.classList.add("bi-directional"); + line.style.stroke = "rgba(255, 255, 255, 0.25)"; + } else { + line.style.stroke = "rgba(255, 255, 255, 0.15)"; } svg.appendChild(line); + + // Explicitly render arrowheads near the middle of the edge to avoid any node container overlap + const dx = endPt.x - startPt.x; + const dy = endPt.y - startPt.y; + const length = Math.hypot(dx, dy); + if (length > 20) { + const ux = dx / length; + const uy = dy / length; + const mx = (startPt.x + endPt.x) / 2; + const my = (startPt.y + endPt.y) / 2; + const arrowLen = 9; + const arrowHalfWidth = 4.5; + const arrowFill = "rgba(255, 255, 255, 0.3)"; + + // Helper to append a polygonal arrowhead given tip position and normalized vector pointing to tip + const drawArrowhead = (tipX, tipY, vx, vy) => { + const baseX = tipX - arrowLen * vx; + const baseY = tipY - arrowLen * vy; + // Perpendicular vector (-vy, vx) + const c1x = baseX - arrowHalfWidth * -vy; + const c1y = baseY - arrowHalfWidth * vx; + const c2x = baseX + arrowHalfWidth * -vy; + const c2y = baseY + arrowHalfWidth * vx; + + const poly = document.createElementNS( + "http://www.w3.org/2000/svg", + "polygon" + ); + poly.setAttribute( + "points", + `${tipX},${tipY} ${c1x},${c1y} ${c2x},${c2y}` + ); + poly.setAttribute("fill", arrowFill); + svg.appendChild(poly); + }; + + if (isBidirectional) { + // Calculate 1/4 and 3/4 split points along the line segment + const pt1_4X = startPt.x + 0.25 * dx; + const pt1_4Y = startPt.y + 0.25 * dy; + const pt3_4X = startPt.x + 0.75 * dx; + const pt3_4Y = startPt.y + 0.75 * dy; + + // Forward arrow centered at 3/4 split pointing towards target node + drawArrowhead( + pt3_4X + (arrowLen / 2) * ux, + pt3_4Y + (arrowLen / 2) * uy, + ux, + uy + ); + // Reverse arrow centered at 1/4 split pointing towards source node + drawArrowhead( + pt1_4X - (arrowLen / 2) * ux, + pt1_4Y - (arrowLen / 2) * uy, + -ux, + -uy + ); + } else { + // Single direction arrow pointing to target node exactly at midpoint + drawArrowhead( + mx + (arrowLen / 2) * ux, + my + (arrowLen / 2) * uy, + ux, + uy + ); + } + } } }); - // 2. Draw nodes + // 2. Draw pretty stylized SDK nodes sdks.forEach((sdk, idx) => { const pos = nodePositions[idx]; if (!pos) return; @@ -380,11 +636,12 @@ document.addEventListener("DOMContentLoaded", () => { ); g.setAttribute( "class", - `svg-node ${pos.isCenter ? "center-node" : ""}` + `svg-node ${pos.isRoot ? "root-node" : ""}` ); - const rectWidth = 100; - const rectHeight = 24; + const rectWidth = 110; + const rectHeight = 28; + const colors = getSdkColors(sdk); const rect = document.createElementNS( "http://www.w3.org/2000/svg", @@ -394,6 +651,13 @@ document.addEventListener("DOMContentLoaded", () => { rect.setAttribute("y", pos.y - rectHeight / 2); rect.setAttribute("width", rectWidth); rect.setAttribute("height", rectHeight); + rect.setAttribute("rx", "6"); + rect.setAttribute("ry", "6"); + + // Apply dedicated language color codes inline + rect.style.fill = colors.fill; + rect.style.stroke = colors.stroke; + rect.style.strokeWidth = pos.isRoot ? "2px" : "1.5px"; const text = document.createElementNS( "http://www.w3.org/2000/svg", @@ -401,7 +665,10 @@ document.addEventListener("DOMContentLoaded", () => { ); text.setAttribute("x", pos.x); text.setAttribute("y", pos.y); - text.textContent = sdk; + text.textContent = formatSdkName(sdk); + text.style.fill = colors.text; + text.style.fontSize = "11px"; + text.style.fontWeight = "600"; g.appendChild(rect); g.appendChild(text); @@ -416,17 +683,17 @@ document.addEventListener("DOMContentLoaded", () => { tbody.innerHTML = ""; - PROTOCOLS.forEach((proto) => { + BEHAVIORS.forEach((behavior) => { const tr = document.createElement("tr"); - // Cell 1: Protocol Name - const protocolTd = document.createElement("td"); - protocolTd.className = "protocol-name"; - protocolTd.textContent = proto; - tr.appendChild(protocolTd); + // Cell 1: Behavior Name + const behaviorTd = document.createElement("td"); + behaviorTd.className = "protocol-name"; + behaviorTd.textContent = behavior.label; + tr.appendChild(behaviorTd); - // Cells 2..5: Behaviors - BEHAVIORS.forEach((behavior) => { + // Cells 2..N: Protocols + PROTOCOLS.forEach((proto) => { const td = document.createElement("td"); // Find all scenarios in the group that cover this Protocol and Behavior diff --git a/itk-dashboard/index.html b/itk-dashboard/index.html index 0f105b5d..68350f2f 100644 --- a/itk-dashboard/index.html +++ b/itk-dashboard/index.html @@ -26,18 +26,6 @@

A2A SDKs Compatibility

-
-
- Last Nightly Run - Loading... -
-
- Commit SHA - Loading... -
-
@@ -110,16 +98,25 @@

Nightly Runs History

-
-

Active Compatibility Profiles

-

- Visual topology graphs and corresponding - compatibility matrices for protocols and behaviors. -

-
+ +
+
+

SUMMARY

+
+ +
+
-
- +
+

+ Pairwise Interoperability Matrices +

+
+ +
+
diff --git a/itk-dashboard/style.css b/itk-dashboard/style.css index 04768ae3..b66f1219 100644 --- a/itk-dashboard/style.css +++ b/itk-dashboard/style.css @@ -484,7 +484,6 @@ a:hover { stroke: rgb(255 255 255 / 10%); stroke-width: 2px; fill: none; - marker-end: url("#arrow"); transition: all 0.3s; } @@ -704,3 +703,61 @@ a:hover { background: #1e293b; border: 1px solid var(--primary); } + +/* --- UNIFIED SINGLE-PAGE STATIC LAYOUT STYLES --- */ + +.dashboard-unified-layout { + display: flex; + flex-direction: column; + gap: 40px; + margin-top: 24px; + width: 100%; +} + +.section-title-unified { + font-size: 16px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--primary); + margin-bottom: 16px; + padding-left: 8px; + border-left: 3px solid var(--primary); + text-shadow: 0 0 10px rgb(129 140 248 / 30%); +} + +.summary-section { + width: 100%; +} + +.pairwise-section { + width: 100%; +} + +.pairwise-grid-layout { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); + gap: 24px; + width: 100%; +} + +/* Make sure the top summary card stretches wide */ +#summary-container .topology-card { + width: 100%; + max-width: 100%; +} + +/* Pairwise graphs and tables stack vertically inside cards for maximum clarity and visibility */ +.pairwise-grid-layout .topology-visual-grid { + grid-template-columns: 1fr; + gap: 20px; +} + +.pairwise-grid-layout .topology-graph-canvas { + min-height: 220px; + padding: 8px; +} + +.pairwise-grid-layout .topology-graph-canvas svg { + height: 220px; +}