diff --git a/doc/newsfragments/3037_new.resource_monitor.rst b/doc/newsfragments/3037_new.resource_monitor.rst new file mode 100755 index 000000000..c5036338a --- /dev/null +++ b/doc/newsfragments/3037_new.resource_monitor.rst @@ -0,0 +1 @@ +Add a new summary page on resource view to show the task allocation per host. \ No newline at end of file diff --git a/testplan/web_ui/testing/src/AssertionPane/ResourcePanel.js b/testplan/web_ui/testing/src/AssertionPane/ResourcePanel.js index 327556007..9ce0a6737 100755 --- a/testplan/web_ui/testing/src/AssertionPane/ResourcePanel.js +++ b/testplan/web_ui/testing/src/AssertionPane/ResourcePanel.js @@ -24,7 +24,9 @@ import prettyBytes from "pretty-bytes"; import { getResourceUrl, timeToTimestamp } from "../Common/utils"; import { RED, + LIGHT_RED, GREEN, + LIGHT_GREEN, BLUE, DARK_BLUE, TEAL, @@ -78,6 +80,12 @@ const prettySizeTicks = (options) => { }; }; +const normalizeStartEndTime = (timer) => { + const start = timer[0]?.setup?.start || timer[0]?.run?.start; + const end = timer[0]?.teardown?.end || timer[0]?.run?.end; + return start && end ? [start, end] : null; +}; + const AnchorDiv = ({ elementIds }) => { const anchorDiv = []; for (let uid of elementIds) { @@ -219,6 +227,10 @@ const TimerGraph = ({ timerEntries, startTime, endTime }) => { { data: [], backgroundColor: [], + borderColor: [], + borderWidth: 2, + borderRadius: Number.MAX_VALUE, + borderSkipped: false, barThickness: 6, minBarLength: 2, }, @@ -227,26 +239,13 @@ const TimerGraph = ({ timerEntries, startTime, endTime }) => { timerEntries.forEach((entity) => { labels.push(entity.name); datasets[0].backgroundColor.push( - // TODO: move color to a constant map + entity.status === STATUS_CATEGORY.passed ? LIGHT_GREEN : LIGHT_RED + ); + datasets[0].borderColor.push( entity.status === STATUS_CATEGORY.passed ? GREEN : RED ); - let start = null; - if (!_.isNil(entity.timer[0].setup?.start)) { - start = entity.timer[0].setup.start; - } else if (!_.isNil(entity.timer[0].run?.start)) { - start = entity.timer[0].run.start; - } else { - datasets[0].data.push(null); - return; - } - - if (!_.isNil(entity.timer[0].teardown?.end)) { - datasets[0].data.push([start, entity.timer[0].teardown.end]); - } else if (!_.isNil(entity.timer[0].run?.end)) { - datasets[0].data.push([start, entity.timer[0].run.end]); - } else { - datasets[0].data.push(null); - } + const normalizedTime = normalizeStartEndTime(entity.timer); + datasets[0].data.push(normalizedTime); }); const height = 10 + timerEntries.length * 5; return ( @@ -569,52 +568,84 @@ HostResource.propTypes = { endTime: PropTypes.number, }; -const TopUsageBanner = ({ maxCPU, maxMemory, maxDisk, maxIOPS }) => { +const TopBanner = ({ + maxCPU, + maxMemory, + maxDisk, + maxIOPS, + showSummary, + setShowSummary, +}) => { const itemStyle = { padding: "8px", - border: "1px solid #0597ff", - backgroundColor: "#e5f0f9", borderRadius: "2px", - color: "#115598", fontSize: "14px", fontWeight: "500", cursor: "pointer", }; + const itemBlueStyle = { + ...itemStyle, + border: "1px solid #0597ff", + backgroundColor: "#e5f0f9", + color: "#115598", + }; + const itemGreenStyle = { + ...itemStyle, + border: "1px solid #00875a", + backgroundColor: "#bcefc8", + color: "#0a5b19", + }; + const cpuDiv = _.isNil(maxCPU?.value, true) ? null : ( -
+
Max CPU Usage: {maxCPU.value}%
); const memDiv = _.isNil(maxMemory?.value) ? null : ( -
+
Max Memory Usage: {prettyBytes(maxMemory.value, { binary: true })}
); const diskDiv = _.isNil(maxDisk?.value) ? null : ( -
+
Max Disk Usage: {prettyBytes(maxDisk.value)}
); const iopsDiv = _.isNil(maxIOPS?.value) ? null : ( -
+
Max IOPS: {maxIOPS.value.toFixed(2)}
); + const showSummaryDiv = ( +
{ + setShowSummary(!showSummary); + }} + > + {showSummary ? "Show Detail" : "Show Resource"} +
+ ); return (
- {cpuDiv} - {memDiv} - {diskDiv} - {iopsDiv} +
+ {cpuDiv} + {memDiv} + {diskDiv} + {iopsDiv} +
+
{showSummaryDiv}
); }; @@ -624,12 +655,14 @@ const TopResourceShap = { uid: PropTypes.string, }; -TopUsageBanner.propTypes = { +TopBanner.propTypes = { maxCPU: PropTypes.shape(TopResourceShap), maxMemory: PropTypes.shape(TopResourceShap), maxDisk: PropTypes.shape(TopResourceShap), maxIOPS: PropTypes.shape(TopResourceShap), onClickCallBack: PropTypes.func, + showSummary: PropTypes.bool, + setShowSummary: PropTypes.func, }; const maxVal = (meta, maxRef, hostTag) => { @@ -654,12 +687,23 @@ const maxVal = (meta, maxRef, hostTag) => { } }; +const HeaderPanelMargin = () => { + return ( +
+ ); +}; + const ResourceContainer = ({ resourceMetaPath, resourceEntries, timerEntries, testStartTime, testEndTime, + showSummary, + setShowSummary, }) => { const routeMatch = useRouteMatch(); @@ -675,13 +719,11 @@ const ResourceContainer = ({ } }, [routeMatch]); - let usageBanner; const hostContent = []; let hostIndex = 0; + const maxRes = {}; if (!_.isEmpty(resourceEntries)) { - const maxRes = {}; - for (let host in resourceEntries) { hostIndex++; const hostMeta = resourceEntries[host].metaData; @@ -703,10 +745,10 @@ const ResourceContainer = ({ hostContent.push(hostDiv); } } - if (!_.isEmpty(maxRes)) { - usageBanner = ; - } } + const usageBanner = ( + + ); const timerContent = []; if (!_.isEmpty(timerEntries)) { @@ -732,10 +774,7 @@ const ResourceContainer = ({ return ( <> {usageBanner} -
+ {hostContent} {timerContent} @@ -748,6 +787,155 @@ ResourceContainer.propTypes = { timerEntries: PropTypes.object, testStartTime: PropTypes.number, testEndTime: PropTypes.number, + showSummary: PropTypes.bool, + setShowSummary: PropTypes.func, +}; + +const SummaryContainer = ({ + resourceEntries, + timerEntries, + testStartTime, + testEndTime, + showSummary, + setShowSummary, +}) => { + const labels = []; + const datasets = []; + for (const hostId in resourceEntries) { + resourceEntries[hostId].timer.forEach((entity) => { + const timeInfo = normalizeStartEndTime(entity.timer); + if (timeInfo) { + datasets.push({ + label: entity.name, + data: [...Array(labels.length).fill(null), timeInfo], + borderColor: entity.status === STATUS_CATEGORY.passed ? GREEN : RED, + borderWidth: 2, + borderRadius: Number.MAX_VALUE, + borderSkipped: false, + backgroundColor: + entity.status === STATUS_CATEGORY.passed ? LIGHT_GREEN : LIGHT_RED, + barThickness: 6, + minBarLength: 2, + }); + } + }); + labels.push(resourceEntries[hostId].metaData.hostname); + } + for (const hostName in timerEntries) { + timerEntries[hostName].forEach((entity) => { + const timeInfo = normalizeStartEndTime(entity.timer); + if (timeInfo) { + datasets.push({ + label: entity.name, + data: [...Array(labels.length).fill(null), timeInfo], + borderColor: entity.status === STATUS_CATEGORY.passed ? GREEN : RED, + borderWidth: 2, + borderRadius: Number.MAX_VALUE, + borderSkipped: false, + backgroundColor: + entity.status === STATUS_CATEGORY.passed ? LIGHT_GREEN : LIGHT_RED, + barThickness: 6, + minBarLength: 2, + }); + } + }); + labels.push(hostName); + } + + const height = 10 + labels.length * 5; + const timezoneOffset = new Date().getTimezoneOffset(); + const summaryGraph = ( + { + return `${dateFormat( + dateAddMinutes(value, timezoneOffset), + "H:mm:ss" + )}Z`; + }, + }, + }, + y: { + stacked: true, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + callbacks: { + label: (context) => { + return [ + context.dataset.label, + `From: ${dateFormat( + dateAddMinutes(context.raw[0], timezoneOffset), + "H:mm:ss" + )}Z To: ${dateFormat( + dateAddMinutes(context.raw[1], timezoneOffset), + "H:mm:ss" + )}Z`, + `Duration: ${( + (context.raw[1] - context.raw[0]) / + 1000 + ).toFixed(2)}s`, + ]; + }, + }, + }, + }, + }} + data={{ + labels: labels, + datasets: datasets, + }} + /> + ); + + return ( + <> + + +
+ {summaryGraph} +
+ + ); +}; + +SummaryContainer.propTypes = { + resourceEntries: PropTypes.object, + timerEntries: PropTypes.object, + testStartTime: PropTypes.number, + testEndTime: PropTypes.number, + showSummary: PropTypes.bool, + setShowSummary: PropTypes.func, }; const normalizeTimer = (timer) => { @@ -816,6 +1004,7 @@ const extractTimeInfo = (report) => { const ResourcePanel = ({ report }) => { const [resourceMeta, setResourceMeta] = useState(null); const [errorInfo, setErrorInfo] = useState(null); + const [showSummary, setShowSummary] = useState(false); useEffect(() => { if (resourceMeta || errorInfo) { @@ -880,16 +1069,31 @@ const ResourcePanel = ({ report }) => { ? new Date(report.timer.run[0].end).getTime() : new Date(report.timer.run.end).getTime(); - content = ( - - ); + if (showSummary) { + content = ( + + ); + } else { + content = ( + + ); + } } else { content =
Loading
; } diff --git a/testplan/web_ui/testing/src/Common/defaults.js b/testplan/web_ui/testing/src/Common/defaults.js index 3b2a8066a..eb11e6f5f 100644 --- a/testplan/web_ui/testing/src/Common/defaults.js +++ b/testplan/web_ui/testing/src/Common/defaults.js @@ -12,8 +12,10 @@ const ROSE = "#fe064466"; const DARK_ROSE = "#be0433"; const GREEN = "#228F1D"; const DARK_GREEN = "#1A721D"; +const LIGHT_GREEN = "#DFF0D8"; const RED = "#A2000C"; const DARK_RED = "#840008"; +const LIGHT_RED = "#F2DEDE"; const ORANGE = "#D2691E"; const DARK_ORANGE = "#DD8800"; const LIGHT_GREY = "#F3F3F3"; @@ -270,8 +272,10 @@ export { DARK_ROSE, GREEN, DARK_GREEN, + LIGHT_GREEN, RED, DARK_RED, + LIGHT_RED, ORANGE, DARK_ORANGE, LIGHT_GREY,