From e8fc5c74ecd99b5760613f0a09eeef75d58fcc75 Mon Sep 17 00:00:00 2001 From: Sumit6307 Date: Wed, 4 Mar 2026 23:53:16 +0530 Subject: [PATCH 1/3] feat: implement Release Dashboard view (#1122) --- .../routes/getReleaseSummary.js | 96 +++++++++++++ TestResultSummaryService/routes/index.js | 1 + .../src/Dashboard/Dashboard.jsx | 5 + .../src/Dashboard/Defaults.js | 10 ++ .../Dashboard/Widgets/ReleaseHealthWidget.jsx | 129 ++++++++++++++++++ .../src/Dashboard/Widgets/index.js | 1 + 6 files changed, 242 insertions(+) create mode 100644 TestResultSummaryService/routes/getReleaseSummary.js create mode 100644 test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx diff --git a/TestResultSummaryService/routes/getReleaseSummary.js b/TestResultSummaryService/routes/getReleaseSummary.js new file mode 100644 index 00000000..ce1401f3 --- /dev/null +++ b/TestResultSummaryService/routes/getReleaseSummary.js @@ -0,0 +1,96 @@ +const { TestResultsDB } = require('../Database'); + +/** + * getReleaseSummary returns health metrics for major JDK releases + * + * @route GET /api/getReleaseSummary + * @group Release - Operations about releases + * @return {object} - releaseSummary - map of jdk versions to their health stats + */ + +module.exports = async (req, res) => { + try { + const testResultsDB = new TestResultsDB(); + + // We want to aggregate health across recent builds for each major JDK version + const aggregateQuery = [ + { + // Only look at builds from the last 7 days to keep dashboard relevant + $match: { + timestamp: { $gt: Date.now() - 7 * 24 * 60 * 60 * 1000 }, + // Focus on actual test/perf jobs + buildName: { $regex: /^(Test|Perf)_openjdk/ }, + }, + }, + { + // Extract JDK version from buildName (e.g., Test_openjdk11_... -> 11) + $addFields: { + jdkVersionMatch: { + $regexFind: { + input: '$buildName', + regex: /openjdk(\d+)/, + }, + }, + }, + }, + { + $addFields: { + jdkVersion: '$jdkVersionMatch.captures', + }, + }, + { + // Flatten the captures array to get the actual version string + $unwind: '$jdkVersion', + }, + { + // Group by JDK version and build result + $group: { + _id: { + jdkVersion: '$jdkVersion', + result: '$buildResult', + }, + count: { $sum: 1 }, + }, + }, + { + // Reshape to make it easier for the frontend + $group: { + _id: '$_id.jdkVersion', + results: { + $push: { + result: '$_id.result', + count: '$count', + }, + }, + }, + }, + { + $sort: { _id: 1 }, + }, + ]; + + const result = await testResultsDB.aggregate(aggregateQuery); + + // Transform the array into a more convenient object format + const summary = {}; + result.forEach((item) => { + const stats = { + SUCCESS: 0, + FAILURE: 0, + UNSTABLE: 0, + ABORTED: 0, + total: 0, + }; + item.results.forEach((res) => { + const r = res.result || 'UNKNOWN'; + stats[r] = res.count; + stats.total += res.count; + }); + summary[item._id] = stats; + }); + + res.send(summary); + } catch (error) { + res.status(500).send({ error: error.message }); + } +}; diff --git a/TestResultSummaryService/routes/index.js b/TestResultSummaryService/routes/index.js index ccb40135..2a10dde4 100644 --- a/TestResultSummaryService/routes/index.js +++ b/TestResultSummaryService/routes/index.js @@ -54,6 +54,7 @@ app.get('/getFeedbackUrl', require('./getFeedbackUrl')); app.get('/rescanBuild', require('./rescanBuild')); app.get('/testParserViaFile', require('./test/testParserViaFile')); app.get('/testParserViaLogStream', require('./test/testParserViaLogStream')); +app.get('/getReleaseSummary', require('./getReleaseSummary')); app.get('/updateComments', require('./updateComments')); app.get('/updateKeepForever', require('./updateKeepForever')); diff --git a/test-result-summary-client/src/Dashboard/Dashboard.jsx b/test-result-summary-client/src/Dashboard/Dashboard.jsx index fbf20703..e7004094 100644 --- a/test-result-summary-client/src/Dashboard/Dashboard.jsx +++ b/test-result-summary-client/src/Dashboard/Dashboard.jsx @@ -18,6 +18,11 @@ export default class Dashboard extends Component { key: '2', children: , }, + { + label: 'Release', + key: '3', + children: , + }, ]} /> ); diff --git a/test-result-summary-client/src/Dashboard/Defaults.js b/test-result-summary-client/src/Dashboard/Defaults.js index a7f9f6ea..90e8e54b 100644 --- a/test-result-summary-client/src/Dashboard/Defaults.js +++ b/test-result-summary-client/src/Dashboard/Defaults.js @@ -25,4 +25,14 @@ export default { }, ], }, + Release: { + widgets: [ + { + type: 'ReleaseHealthWidget', + x: 0, + y: 0, + settings: {}, + }, + ], + }, }; diff --git a/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx b/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx new file mode 100644 index 00000000..1b1809a5 --- /dev/null +++ b/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx @@ -0,0 +1,129 @@ +import React, { Component } from 'react'; +import { Table, Tooltip, Progress, Typography } from 'antd'; +import { fetchData } from '../../../utils/Utils'; + +const { Title: AntTitle } = Typography; + +export default class ReleaseHealthWidget extends Component { + static Title = (props) => 'Release Health Overview'; + static defaultSize = { w: 4, h: 4 }; + static defaultSettings = {}; + + state = { + data: {}, + loading: true, + }; + + async componentDidMount() { + await this.updateData(); + // Update every 5 minutes + this.intervalId = setInterval( + () => { + this.updateData(); + }, + 5 * 60 * 1000 + ); + } + + componentWillUnmount() { + clearInterval(this.intervalId); + } + + updateData = async () => { + this.setState({ loading: true }); + const summary = await fetchData('/api/getReleaseSummary'); + if (summary) { + this.setState({ data: summary, loading: false }); + } else { + this.setState({ loading: false }); + } + }; + + render() { + const { data, loading } = this.state; + + const tableData = Object.keys(data).map((jdkVersion) => { + const stats = data[jdkVersion]; + const passRate = + stats.total > 0 + ? Math.round((stats.SUCCESS / stats.total) * 100) + : 0; + + return { + key: jdkVersion, + jdkVersion: `JDK ${jdkVersion}`, + success: stats.SUCCESS, + failure: stats.FAILURE, + unstable: stats.UNSTABLE, + total: stats.total, + passRate: passRate, + }; + }); + + const columns = [ + { + title: 'JDK Version', + dataIndex: 'jdkVersion', + key: 'jdkVersion', + sorter: (a, b) => parseInt(a.key) - parseInt(b.key), + }, + { + title: 'Success', + dataIndex: 'success', + key: 'success', + render: (val) => ( + {val} + ), + }, + { + title: 'Failure', + dataIndex: 'failure', + key: 'failure', + render: (val) => ( + {val} + ), + }, + { + title: 'Unstable', + dataIndex: 'unstable', + key: 'unstable', + render: (val) => ( + {val} + ), + }, + { + title: 'Health', + dataIndex: 'passRate', + key: 'passRate', + render: (percent) => ( + + 90 + ? 'success' + : percent > 70 + ? 'normal' + : 'exception' + } + /> + + ), + sorter: (a, b) => a.passRate - b.passRate, + }, + ]; + + return ( +
+ + + ); + } +} diff --git a/test-result-summary-client/src/Dashboard/Widgets/index.js b/test-result-summary-client/src/Dashboard/Widgets/index.js index b42b4635..4a9fe238 100644 --- a/test-result-summary-client/src/Dashboard/Widgets/index.js +++ b/test-result-summary-client/src/Dashboard/Widgets/index.js @@ -1,2 +1,3 @@ export * from './BuildStatus/'; export * from './Graph/'; +export { default as ReleaseHealthWidget } from './ReleaseHealthWidget'; From 64899546ee6ddf8fd92b53dffce1d2526bc6c8ca Mon Sep 17 00:00:00 2001 From: Sumit6307 Date: Thu, 5 Mar 2026 00:01:37 +0530 Subject: [PATCH 2/3] feat: complete Release Dashboard epic and enhance MetricsDetails with save functionality --- TestResultSummaryService/routes/index.js | 1 + .../src/TrafficLight/MetricsTable.jsx | 179 +++++++++--------- test-result-summary-client/src/utils/Utils.js | 19 ++ 3 files changed, 114 insertions(+), 85 deletions(-) diff --git a/TestResultSummaryService/routes/index.js b/TestResultSummaryService/routes/index.js index 2a10dde4..0e67e533 100644 --- a/TestResultSummaryService/routes/index.js +++ b/TestResultSummaryService/routes/index.js @@ -58,6 +58,7 @@ app.get('/getReleaseSummary', require('./getReleaseSummary')); app.get('/updateComments', require('./updateComments')); app.get('/updateKeepForever', require('./updateKeepForever')); +app.post('/updateStats', require('./updateStats')); // jwt app.post('/auth/register', require('./jwt/register')); diff --git a/test-result-summary-client/src/TrafficLight/MetricsTable.jsx b/test-result-summary-client/src/TrafficLight/MetricsTable.jsx index 4cd0bc91..63bba380 100644 --- a/test-result-summary-client/src/TrafficLight/MetricsTable.jsx +++ b/test-result-summary-client/src/TrafficLight/MetricsTable.jsx @@ -21,7 +21,7 @@ const SummaryRow = ({ type, stats }) => { ); }; -const MetricsTable = ({ type, id, benchmarkName }) => { +const MetricsTable = ({ type, id, benchmarkName, onExcludedRunsChange }) => { const [data, setData] = useState([]); const [javaVersion, setJavaVersion] = useState([]); useEffect(() => { @@ -32,42 +32,46 @@ const MetricsTable = ({ type, id, benchmarkName }) => { } if (results && results[0]) { const aggregateInfo = results[0].aggregateInfo; - const fliteredData = Object.values(aggregateInfo).find( + const filteredItem = Object.values(aggregateInfo).find( (item) => benchmarkName === item.benchmarkName && item.buildName.includes(type) ); - const [firstMetric] = fliteredData.metrics; - const rawValues = firstMetric.rawValues.map((_, i) => { - return { - key: i, - iteration: i, - enabled: true, - metrics: fliteredData.metrics.map((metric) => { - return { - metricName: metric.name, - value: metric.rawValues[i], - }; - }) - }; - }); - const grandchildrenData = await fetchData( - `/api/getChildBuilds?parentId=${results[0]._id}&buildName=${fliteredData.buildName}` - ); - let javaVersion = ''; - for (const grandchildData of grandchildrenData) { - if (grandchildData.javaVersion) { - javaVersion = grandchildData.javaVersion; - break; + if (filteredItem) { + const [firstMetric] = filteredItem.metrics; + const excludedIndices = filteredItem.excludedRuns || []; + const rawValues = firstMetric.rawValues.map((_, i) => { + return { + key: i, + iteration: i, + enabled: !excludedIndices.includes(i), + metrics: filteredItem.metrics.map((metric) => { + return { + metricName: metric.name, + value: metric.rawValues[i], + }; + }), + buildName: filteredItem.buildName, + }; + }); + const grandchildrenData = await fetchData( + `/api/getChildBuilds?parentId=${results[0]._id}&buildName=${filteredItem.buildName}` + ); + let javaVersion = ''; + for (const grandchildData of grandchildrenData) { + if (grandchildData.javaVersion) { + javaVersion = grandchildData.javaVersion; + break; + } } + setJavaVersion(javaVersion); + setData(rawValues); } - setJavaVersion(javaVersion); - setData(rawValues); } }; updateData(); - }, []); + }, [id, benchmarkName]); const handleToggle = (record) => { const newData = data.map((item) => { @@ -76,53 +80,64 @@ const MetricsTable = ({ type, id, benchmarkName }) => { } return item; }); - setData(newData); + setData(newData); + + if (onExcludedRunsChange) { + const excludedIndices = newData + .filter((item) => !item.enabled) + .map((item) => item.iteration); + onExcludedRunsChange(record.buildName, excludedIndices); + } }; // const uniqueTitle = [...new Set(data.map((item) => item.metricName))]; const columns = [ - { - title: 'Iteration', - dataIndex: 'iteration', - key: 'iteration', - width: 100, - render: (iteration) => `Run ${iteration + 1}`, - }, - ...(data[0]?.metrics.map(({ metricName }, i) => { - return { - title: metricName, - key: metricName, - render: (_, record) => { + { + title: 'Iteration', + dataIndex: 'iteration', + key: 'iteration', + width: 100, + render: (iteration) => `Run ${iteration + 1}`, + }, + ...(data[0]?.metrics.map(({ metricName }, i) => { + return { + title: metricName, + key: metricName, + render: (_, record) => { + return ( +
+ {record.metrics[i].value} +
+ ); + }, + }; + }) ?? []), + { + title: 'Annotate data outliers', + dataIndex: 'enabled', + key: 'enabled', + width: 150, + fixed: 'right', + render: (enabled, record) => { return ( -
- {record.metrics[i].value} -
+ handleToggle(record)} + checkedChildren={ } + unCheckedChildren="Exclude data" + /> ); }, - }; - }) ?? []), - { - title: 'Annotate data outliers', - dataIndex: 'enabled', - key: 'enabled', - width: 150, - fixed: 'right', - render: (enabled, record) => { - return ( - handleToggle(record)} - checkedChildren={ } - unCheckedChildren="Exclude data" - /> - ); }, - }, -]; + ]; return (
@@ -135,9 +150,11 @@ const MetricsTable = ({ type, id, benchmarkName }) => {

JDK Version: {javaVersion}
- - Enabled: {data.filter(d => d.enabled).length} / {data.length} iterations - + + Enabled:{' '} + {data.filter((d) => d.enabled).length} /{' '} + {data.length} iterations +
{ columns={columns} pagination={{ defaultPageSize: 50 }} summary={(pageData) => { - const enabledData = pageData.filter(item => item.enabled); + const enabledData = pageData.filter((item) => item.enabled); if (!enabledData.length) return null; - const pivot = zip(...enabledData.map(d => d.metrics)); - + const pivot = zip(...enabledData.map((d) => d.metrics)); + const stats = pivot.map((p) => { - const values = p.map(({ value }) => value) - const mean = Number( - math.mean(values) - ).toFixed(0); + const values = p.map(({ value }) => value); + const mean = Number(math.mean(values)).toFixed(0); const max = math.max(values); const min = math.min(values); - const median = Number( - math.median(values) - ).toFixed(0); - const std = Number( - math.std(values) - ).toFixed(2); + const median = Number(math.median(values)).toFixed(0); + const std = Number(math.std(values)).toFixed(2); const CI = Number( - BenchmarkMath.confidence_interval( - values - ) * 100 + BenchmarkMath.confidence_interval(values) * 100 ).toFixed(2) + '%'; return { mean, diff --git a/test-result-summary-client/src/utils/Utils.js b/test-result-summary-client/src/utils/Utils.js index e141e7bb..c3b4328f 100644 --- a/test-result-summary-client/src/utils/Utils.js +++ b/test-result-summary-client/src/utils/Utils.js @@ -89,3 +89,22 @@ export const fetchData = async (url) => { console.error(e); } }; + +export const postData = async (url, data) => { + if (connectAdoptiumAPI) { + url = 'https://trss.adoptium.net' + url; + } + try { + const response = await fetch(url, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + const jsonResponse = await response.json(); + return jsonResponse; + } catch (e) { + console.error(e); + } +}; From cbb641da98dfcacfc4aca7ae8b99d8399bdb3239 Mon Sep 17 00:00:00 2001 From: Sumit6307 Date: Thu, 5 Mar 2026 00:10:14 +0530 Subject: [PATCH 3/3] fix: resolve CI build failures by fixing import paths and removing unused variables --- .../src/Dashboard/Widgets/ReleaseHealthWidget.jsx | 4 +--- test-result-summary-client/src/TrafficLight/MetricsTable.jsx | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx b/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx index 1b1809a5..fb383aad 100644 --- a/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx +++ b/test-result-summary-client/src/Dashboard/Widgets/ReleaseHealthWidget.jsx @@ -1,8 +1,6 @@ import React, { Component } from 'react'; import { Table, Tooltip, Progress, Typography } from 'antd'; -import { fetchData } from '../../../utils/Utils'; - -const { Title: AntTitle } = Typography; +import { fetchData } from '../../utils/Utils'; export default class ReleaseHealthWidget extends Component { static Title = (props) => 'Release Health Overview'; diff --git a/test-result-summary-client/src/TrafficLight/MetricsTable.jsx b/test-result-summary-client/src/TrafficLight/MetricsTable.jsx index 63bba380..1e31c3e5 100644 --- a/test-result-summary-client/src/TrafficLight/MetricsTable.jsx +++ b/test-result-summary-client/src/TrafficLight/MetricsTable.jsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { Table, Typography, Switch } from 'antd'; -import { DeleteTwoTone } from '@ant-design/icons'; import { fetchData } from '../utils/Utils'; import BenchmarkMath from '../utils/BenchmarkMathCalculation'; import * as math from 'mathjs';