From 06ae543103a265d305668e650954f214dff2989b Mon Sep 17 00:00:00 2001 From: DouPache Date: Thu, 17 Oct 2024 15:45:12 -0500 Subject: [PATCH] [YUNIKORN-2349] Allow changing the orientation of the queue graph (#202) Closes: #202 Signed-off-by: Craig Condit --- .../queue-v2/queues-v2.component.html | 18 +- .../queue-v2/queues-v2.component.scss | 45 +- .../queue-v2/queues-v2.component.ts | 590 ++++++++++-------- src/app/utils/common.util.spec.ts | 21 +- src/app/utils/common.util.ts | 24 +- 5 files changed, 406 insertions(+), 292 deletions(-) diff --git a/src/app/components/queue-v2/queues-v2.component.html b/src/app/components/queue-v2/queues-v2.component.html index d1397eda..cfa6fc9f 100644 --- a/src/app/components/queue-v2/queues-v2.component.html +++ b/src/app/components/queue-v2/queues-v2.component.html @@ -21,12 +21,18 @@
Partition
- + +
diff --git a/src/app/components/queue-v2/queues-v2.component.scss b/src/app/components/queue-v2/queues-v2.component.scss index 8f0f1efe..3c5e597c 100644 --- a/src/app/components/queue-v2/queues-v2.component.scss +++ b/src/app/components/queue-v2/queues-v2.component.scss @@ -39,7 +39,7 @@ font-weight: 600; color: #010407; } - + .fit-to-screen-button { position: relative; display: inline-flex; @@ -51,6 +51,48 @@ border-radius: 5px; cursor: pointer; } + .ort-button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 6px; + background-color: #e5ecf6; + border: 1px solid #132030; + border-radius: 5px; + cursor: pointer; + overflow: show; + } + + .ort-button:hover { + background-color: #8090a5; + } + + .ort-button .tooltip { + width: 100px; + position: absolute; + bottom: 90%; + left: 50%; + transform: translateX(-50%); + background-color: rgb(225, 228, 241); + color: rgb(6, 7, 6); + text-align: center; + padding: 5px 5px; + border-radius: 6px; + visibility: hidden; + opacity: 1; + } + + .ort-button:hover .tooltip { + margin-bottom: 10px; + visibility: visible; + opacity: 1; + } + + .ort-button, + .fit-to-screen-button { + margin-right: 10px; + } .tooltip { width: 100px; @@ -65,7 +107,6 @@ border-radius: 6px; visibility: hidden; opacity: 1; - transition: opacity 0.3s, visibility 0.3s; } .fit-to-screen-button:hover .tooltip { diff --git a/src/app/components/queue-v2/queues-v2.component.ts b/src/app/components/queue-v2/queues-v2.component.ts index fddd0451..156b3524 100644 --- a/src/app/components/queue-v2/queues-v2.component.ts +++ b/src/app/components/queue-v2/queues-v2.component.ts @@ -99,321 +99,367 @@ function queueVisualization(rawData : QueueInfo , componentInstance: QueueV2Comp let isShowingDetails = false; let selectedNode: any = null; - const duration = 750; + // Set this variable to 'horizontal' or 'vertical' to change orientation + // Define a type for the orientation + type Orientation = 'horizontal' | 'vertical'; + + // Declare orientation as the defined type + let orientation: Orientation = 'horizontal'; + + const duration = 500; const svg = select('.visualize-area').append('svg') - .attr('width', '100%') - .attr('height', '100%') - - function fitGraphScale(){ - const baseSvgElem = svg.node() as SVGGElement; - const bounds = baseSvgElem.getBBox(); - const parent = baseSvgElem.parentElement as HTMLElement; - const fullWidth = parent.clientWidth; - const fullHeight = parent.clientHeight; - - const xfactor: number = fullWidth / bounds.width; - const yfactor: number = fullHeight / bounds.height; - let scaleFactor: number = Math.min(xfactor, yfactor); - - // Add some padding so that the graph is not touching the edges - const paddingPercent = 0.9; - scaleFactor = scaleFactor * paddingPercent; - return scaleFactor - } + .attr('width', '100%') + .attr('height', '100%'); + + function fitGraphScale() { + const baseSvgElem = svg.node() as SVGGElement; + const bounds = baseSvgElem.getBBox(); + const parent = baseSvgElem.parentElement as HTMLElement; + const fullWidth = parent.clientWidth; + const fullHeight = parent.clientHeight; + + const xfactor: number = fullWidth / bounds.width; + const yfactor: number = fullHeight / bounds.height; + let scaleFactor: number = Math.min(xfactor, yfactor); - function centerGraph() { - const bbox = (svgGroup.node() as SVGGElement).getBBox(); - const cx = bbox.x + bbox.width / 2; - const cy = bbox.y + bbox.height / 2; - return {cx, cy}; - } + const paddingPercent = 0.9; + scaleFactor = scaleFactor * paddingPercent; + return scaleFactor; + } - function adjustVisulizeArea(duration : number = 0){ - const scaleFactor = fitGraphScale(); - const {cx, cy} = centerGraph(); - // make the total duration to be 1 second - svg.transition().duration(duration/1.5).call(zoom.translateTo, cx, cy) - .on("end", function() { - svg.transition().duration(duration/1.5).call(zoom.scaleBy, scaleFactor) - }) - } + function centerGraph() { + const bbox = (svgGroup.node() as SVGGElement).getBBox(); + const cx = bbox.x + bbox.width / 2; + const cy = bbox.y + bbox.height / 2; + return {cx, cy}; + } - // Append a svg group which holds all nodes and which is for the d3 zoom - const svgGroup = svg.append("g") + function adjustVisulizeArea(duration: number = 0) { + const scaleFactor = fitGraphScale(); + const {cx, cy} = centerGraph(); + svg.transition().duration(duration/1.5).call(zoom.translateTo, cx, cy) + .on("end", function() { + svg.transition().duration(duration/1.5).call(zoom.scaleBy, scaleFactor) + }); + } - const fitButton = select(".fit-to-screen-button") + function changeOrientation() { + orientation = orientation === 'horizontal' ? 'vertical' : 'horizontal'; + const root = d3hierarchy.hierarchy(rawData); + update(root); + + // Update the position of existing plus circles, plus text, and queue names + svgGroup.selectAll('g.card') + .each(function(d: any) { + const group = select(this); + group.select('circle') + .attr("cx", orientation === 'horizontal' ? 300 : 150) + .attr("cy", orientation === 'horizontal' ? 60 : 120); + group.select('.plus-text') + .attr("x", orientation === 'horizontal' ? 300 : 150) + .attr("y", orientation === 'horizontal' ? 67 : 127); + }); + } + + const svgGroup = svg.append("g"); + + const fitButton = select(".fit-to-screen-button") .on("click", function() { - adjustVisulizeArea(duration) + adjustVisulizeArea(duration); }) .on('mouseenter', function() { select(this).select('.tooltip') - .style('visibility', 'visible') - .style('opacity', 1); + .style('visibility', 'visible') + .style('opacity', 1); }) .on('mouseleave', function() { select(this).select('.tooltip') - .style('visibility', 'hidden') - .style('opacity', 0); + .style('visibility', 'hidden') + .style('opacity', 0); }); - - const treelayout = d3flextree - .flextree({}) - .nodeSize((d) => { - return [300, 300]; - } - ) - .spacing(() => 300); - - const zoom = d3zoom + + const ortButton = select(".ort-button") + .on("click", function() { + changeOrientation(); + setTimeout( + () => { + const fitButton = document.getElementById('fitButton'); + if (fitButton) { + fitButton.click(); + } + }, duration); + }); + + + const treelayout = d3flextree + .flextree({}) + .nodeSize((d) => { + return orientation === 'horizontal' ? [300, 600] : [300, 300]; + }) + .spacing(() => orientation === 'horizontal' ? 100 : 300); + + const zoom = d3zoom .zoom() .scaleExtent([0.1, 5]) .on("zoom", (event) => { - svgGroup.attr("transform", event.transform) + svgGroup.attr("transform", event.transform); }); - svg.call(zoom); - - const root = d3hierarchy.hierarchy(rawData); - update(root); + svg.call(zoom); + + const root = d3hierarchy.hierarchy(rawData); + update(root); + + function update(source: any) { + var treeData = treelayout(root); + var nodes = treeData.descendants(); + var node = svgGroup + .selectAll>('g.card') + .data(nodes, function(d: any) { + return d.id || (d.id = ++numberOfNode); + }); - function update(source: any){ - var treeData = treelayout(root) - var nodes = treeData.descendants() - var node = svgGroup - .selectAll>('g.card') - .data(nodes, function(d : any) { - return d.id || (d.id = ++numberOfNode); - }); - - var nodeEnter = node - .enter().append('g') - .attr('class', 'card') - .attr("transform", function() { - if (source.x0 && source.y0) { - return "translate(" + source.x0 + "," + source.y0 + ")"; - } - else { - return "translate(" + source.x + "," + source.y + ")"; - } - }) - - nodeEnter.each(function(d) { - const group = select(this); - const queueName = d.data.queueName?.split(".").at(-1) ?? d.data.queueName; - - group.append("rect") - .attr("width", 300) - .attr("height", 120) - .attr("fill", "none") - .attr("stroke", "white") - .attr("stroke-width", 2) - .attr("rx", 10) - .attr("ry", 10) - .attr("class", "cardMain"); - - group.append("rect") - .attr("width", 300) - .attr("height", 30) - .attr("fill", "#d4eaf7") - .attr("class", "cardTop"); - - group.append("rect") - .attr("y", 30) - .attr("width", 300) - .attr("height", 60) - .attr("fill", "white") - .attr("class", "cardMiddle"); - - group.append("rect") - .attr("y", 90) - .attr("width", 300) - .attr("height", 30) - .attr("fill", "#e6f4ea") - .attr("class", "cardBottom"); - - group.append("image") - .attr("href", "./assets/images/hierarchy.svg") - .attr("x", 5) - .attr("y", 5) - .attr("width", 20) - .attr("height", 20); - - group.append("text") - .attr("x", 30) - .attr("y", 22.5) - .attr("font-size", "25px") - .attr("fill", "black") - .text(queueName) - .call(ellipsis, 270) - .call(tooltip, group, queueName); + var nodeEnter = node + .enter().append('g') + .attr('class', 'card') + .attr("transform", function() { + if (source.y0 !== undefined && source.x0 !== undefined) { + return orientation === 'horizontal' + ? `translate(${source.y0},${source.x0})` + : `translate(${source.x0},${source.y0})`; + } else { + return orientation === 'horizontal' + ? `translate(${source.y},${source.x})` + : `translate(${source.x},${source.y})`; + } + }); + nodeEnter.each(function(d) { + const group = select(this); + const queueName = d.data.queueName?.split(".").at(-1) ?? d.data.queueName; + + group.append("rect") + .attr("width", 300) + .attr("height", 120) + .attr("fill", "none") + .attr("stroke", "white") + .attr("stroke-width", 2) + .attr("rx", 10) + .attr("ry", 10) + .attr("class", "cardMain"); + + group.append("rect") + .attr("width", 300) + .attr("height", 30) + .attr("fill", "#d4eaf7") + .attr("class", "cardTop"); + + group.append("rect") + .attr("y", 30) + .attr("width", 300) + .attr("height", 60) + .attr("fill", "white") + .attr("class", "cardMiddle"); + + group.append("rect") + .attr("y", 90) + .attr("width", 300) + .attr("height", 30) + .attr("fill", "#e6f4ea") + .attr("class", "cardBottom"); + + group.append("image") + .attr("href", "./assets/images/hierarchy.svg") + .attr("x", 5) + .attr("y", 5) + .attr("width", 20) + .attr("height", 20); + + + group.append("text") + .attr("x", 30) + .attr("y", 22.5) + .attr("font-size", "25px") + .attr("fill", "black") + .text(queueName) + .call(ellipsis, 270) + .call(tooltip, group, queueName); + const plusCircle = group.append("circle") - .attr("cx", 150) - .attr("cy", 120) - .attr("r", 20) - .attr("fill", "white") - .attr("stroke", "black") - .attr("stroke-width", 1) - .style("visibility", "hidden") - .on('click', function(event) { - event.stopPropagation(); // Prevents the event from bubbling up to parent elements - click(event, d); - }); - - const plusText = group.append("text") - .attr("x", 150) - .attr("y", 127) - .attr("text-anchor", "middle") - .attr("font-size", "20px") - .attr("fill", "black") - .text("+") - .attr("pointer-events", "none") // Prevents the text from interfering with the click event - .style("visibility", "hidden"); - - group.on("mouseover", function() { - plusCircle.style("visibility", "visible"); - plusText.style("visibility", "visible"); + .attr("cx", () => orientation === 'horizontal' ? 300 : 150) // Right side if horizontal, center if vertical + .attr("cy", () => orientation === 'horizontal' ? 60 : 120) // Center if horizontal, bottom if vertical + .attr("r", 20) + .attr("fill", "white") + .attr("stroke", "black") + .attr("stroke-width", 1) + .style("visibility", "hidden") + .on('click', function(event) { + event.stopPropagation(); + click(event, d); }); + + const plusText = group.append("text") + .classed("plus-text", true) + .attr("x", () => orientation === 'horizontal' ? 300 : 150) + .attr("y", () => orientation === 'horizontal' ? 67 : 127) + .attr("text-anchor", "middle") + .attr("font-size", "20px") + .attr("fill", "black") + .text("+") + .attr("pointer-events", "none") + .style("visibility", "hidden"); + + group.on("mouseover", function() { + plusCircle.style("visibility", "visible"); + plusText.style("visibility", "visible"); + }); - group.on("click", function() { - if(selectedNode == this || selectedNode == null){ - isShowingDetails = !isShowingDetails; - }else{ - //set the previous selected node to its original css - select(selectedNode).select('.cardMain').attr("stroke", "white") - .attr("stroke-width", 2) + group.on("click", function() { + if(selectedNode == this || selectedNode == null){ + isShowingDetails = !isShowingDetails; + } else { + select(selectedNode).select('.cardMain').attr("stroke", "white") + .attr("stroke-width", 2); - select(selectedNode).select('.cardTop').attr("fill", "#d4eaf7") - } + select(selectedNode).select('.cardTop').attr("fill", "#d4eaf7"); + } - selectedNode = this; - componentInstance.seletedInfo = d.data; + selectedNode = this; + componentInstance.seletedInfo = d.data; - if(isShowingDetails){ - console.log("showing details", componentInstance.seletedInfo) - select(this).select('.cardMain').attr("stroke-width", 8) - .attr("stroke", "#50505c") + if(isShowingDetails){ + console.log("showing details", componentInstance.seletedInfo); + select(this).select('.cardMain').attr("stroke-width", 8) + .attr("stroke", "#50505c"); - select(this).select('.cardTop').attr("fill", "#95d5f9") + select(this).select('.cardTop').attr("fill", "#95d5f9"); - select(".additional-info-element").style("display", "block"); - } else { - select(this).select('.cardMain').attr("stroke-width", 2) - .attr("stroke", "white") + select(".additional-info-element").style("display", "block"); + } else { + select(this).select('.cardMain').attr("stroke-width", 2) + .attr("stroke", "white"); - select(this).select('.cardTop').attr("fill", "#d4eaf7") + select(this).select('.cardTop').attr("fill", "#d4eaf7"); - select(".additional-info-element").style("display", "none"); - } + select(".additional-info-element").style("display", "none"); + } - adjustVisulizeArea(duration) - }); - - // Hide the circle and '+' text when the mouse leaves the node - group.on("mouseout", function() { - plusCircle.style("visibility", "hidden"); - plusText.style("visibility", "hidden"); - }); + adjustVisulizeArea(duration); + }); + + group.on("mouseout", function() { + plusCircle.style("visibility", "hidden"); + plusText.style("visibility", "hidden"); + }); - // Add hover effect to the circle to change its color to grey - plusCircle.on("mouseover", function() { - select(this).attr("fill", "grey"); - }); + plusCircle.on("mouseover", function() { + select(this).attr("fill", "grey"); + }); - // Reset circle color when mouse leaves - plusCircle.on("mouseout", function() { - select(this).attr("fill", "white"); - }); + plusCircle.on("mouseout", function() { + select(this).attr("fill", "white"); + }); + }); + const nodeUpdate = nodeEnter.merge(node) + .attr("stroke", "black"); + + nodeUpdate.transition() + .duration(duration) + .attr("transform", function(this: SVGGElement, event: any, i: any, arr: any) { + const d: any = select(this).datum(); + return orientation === 'horizontal' + ? `translate(${d.y},${d.x})` + : `translate(${d.x},${d.y})`; }); + + nodeUpdate.select('.cardBottom') + .style("fill", function(d: any) { + return d._children ? "#9fc6aa" : "#e6f4ea"; + }); + + var nodeExit = node.exit().transition() + .duration(duration) + .attr("transform", function(this: SVGGElement, event: any, i: any, arr: any) { + const d = select(this).datum(); + return orientation === 'horizontal' + ? `translate(${source.y},${source.x})` + : `translate(${source.x},${source.y})`; + }) + .remove(); - const nodeUpdate = nodeEnter.merge(node) + const links = treeData.links(); + let link = svgGroup.selectAll>('path.link') + .data(links, function(d: any) { return d.target.id; }); + + const linkEnter = link.enter().insert('path', "g") + .attr("class", "link") + .attr('d', d => { + const o = orientation === 'horizontal' + ? {y: source.y0 || source.y, x: source.x0 || source.x} + : {x: source.x0 || source.x, y: source.y0 || source.y}; + return diagonal(o, o, orientation); + }) + .attr("fill", "none") .attr("stroke", "black") - - nodeUpdate.transition() - .duration(duration) - .attr("transform", function(this: SVGGElement , event : any , i : any, arr : any) { - const d : any = select(this).datum(); - return "translate(" + d.x + "," + d.y + ")"; - }); - - nodeUpdate.select('.cardBottom') - .style("fill", function(d : any) { - return d._children ? "#9fc6aa" : "#e6f4ea"; + .attr("stroke-width", "2px"); + + const linkUpdate = linkEnter.merge(link); + linkUpdate.transition() + .duration(duration) + .attr('d', d => diagonal(d.source, d.target, orientation)); + + const linkExit = link.exit().transition() + .duration(duration) + .attr('d', d => { + const o = orientation === 'horizontal' + ? {y: source.y, x: source.x} + : {x: source.x, y: source.y}; + return diagonal(o, o, orientation); }) + .remove(); + + nodes.forEach(function(d: any) { + d.x0 = d.x; + d.y0 = d.y; + }); - // Remove any exiting nodes - var nodeExit= node.exit().transition() - .duration(duration) - .attr("transform", function(this: SVGGElement , event : any , i : any, arr : any) { - const d = select(this).datum(); - return "translate(" + source.x + "," + source.y + ")"; - }) - .remove(); - - // Link sections - const links = treeData.links(); - let link = svgGroup.selectAll>('path.link') - .data(links, function(d : any) { return d.target.id; }); - - const linkEnter = link.enter().insert('path', "g") - .attr("class", "link") - .attr('d', d => { - if (source.x0 && source.y0) { - const o = {x: source.x0, y: source.y0}; - return diagonal(o, o); - } - else { - const o = {x: source.x, y: source.y}; - return diagonal(o, o); - } - }) - .attr("fill", "none") - .attr("stroke", "black") - .attr("stroke-width", "2px"); - - const linkUpdate = linkEnter.merge(link); - linkUpdate.transition() - .duration(duration) - .attr('d', d => diagonal(d.source, d.target)); - - const linkExit = link.exit().transition() - .duration(duration) - .attr('d', d => { - const o = {x: source.x, y: source.y}; - return diagonal(o, o); - }) - .remove(); - - nodes.forEach(function(d : any) { - d.x0 = d.x; - d.y0 = d.y; - }); - - function click(event : MouseEvent, d : any) { - if (d.children) { - d._children = d.children; - d.children = null; - } else { - d.children = d._children; - d._children = null; - } - - update(d); + function click(event: MouseEvent, d: any) { + if (d.children) { + d._children = d.children; + d.children = null; + } else { + d.children = d._children; + d._children = null; } + + update(d); } + } } -function diagonal(s : any , d : any) { - const sourceX = s.x + 150; // Middle of the rectangle's width - const sourceY = s.y + 120; // Bottom of the rectangle - const targetX = d.x + 150; // Middle of the rectangle's width - const targetY = d.y; // Top of the rectangle - - return `M ${sourceX} ${sourceY} - V ${(sourceY + targetY) / 2} - H ${targetX} - V ${targetY}`; +function diagonal(s: any, d: any, orientation: string) { + if (orientation == 'horizontal') { + const sourceY = s.y + 300; // Right side of the rectangle + const sourceX = s.x + 60; // Middle of the rectangle's height + const targetY = d.y; // Left side of the target rectangle + const targetX = d.x + 60; // Middle of the target rectangle's height + + return `M ${sourceY} ${sourceX} + H ${(sourceY + targetY) / 2} + V ${targetX} + H ${targetY}`; + } else { + const sourceX = s.x + 150; // Middle of the rectangle's width + const sourceY = s.y + 120; // Bottom of the rectangle + const targetX = d.x + 150; // Middle of the rectangle's width + const targetY = d.y; // Top of the rectangle + + return `M ${sourceX} ${sourceY} + V ${(sourceY + targetY) / 2} + H ${targetX} + V ${targetY}`; + } } function ellipsis( diff --git a/src/app/utils/common.util.spec.ts b/src/app/utils/common.util.spec.ts index 076be592..0319144e 100644 --- a/src/app/utils/common.util.spec.ts +++ b/src/app/utils/common.util.spec.ts @@ -63,28 +63,37 @@ describe('CommonUtil', () => { it('should return an empty string for undefined input', () => { expect(CommonUtil.absoluteUsedMemoryColumnFormatter(undefined)).toBe(''); }); - it('should return "n/a" for "n/a" input', () => { expect(CommonUtil.absoluteUsedMemoryColumnFormatter('n/a')).toBe('Memory: n/a'); }); - it('should format memory percentage correctly', () => { expect(CommonUtil.absoluteUsedMemoryColumnFormatter('Memory: 40%')).toBe('Memory: 40%'); - }); + }); + it('should handle input without percentage sign', () => { + expect(CommonUtil.absoluteUsedMemoryColumnFormatter('40')).toBe('Memory: n/a (wrong memory format)'); + }); + it('should handle incorrect input', () => { + expect(CommonUtil.absoluteUsedMemoryColumnFormatter('cpumMMEORY')).toBe('Memory: n/a (wrong memory format)'); + }); }); describe('checkin absoluteUsedCPUColumnFormatter method result', () => { it('should return an empty string for undefined input', () => { - expect(CommonUtil.absoluteUsedMemoryColumnFormatter(undefined)).toBe(''); + expect(CommonUtil.absoluteUsedCPUColumnFormatter(undefined)).toBe(''); }); - it('should return "n/a" for "n/a" input', () => { expect(CommonUtil.absoluteUsedCPUColumnFormatter('n/a')).toBe('CPU: n/a'); }); it('should format memory percentage correctly', () => { - expect(CommonUtil.absoluteUsedMemoryColumnFormatter('CPU: 60%')).toBe('CPU: 60%'); + expect(CommonUtil.absoluteUsedCPUColumnFormatter('CPU: 60%')).toBe('CPU: 60%'); }); + it('should handle input without percentage sign', () => { + expect(CommonUtil.absoluteUsedCPUColumnFormatter('60')).toBe('CPU: n/a (wrong cpu format)'); + }); + it('should handle incorrect input', () => { + expect(CommonUtil.absoluteUsedCPUColumnFormatter('cpumMMEORY')).toBe('CPU: n/a (wrong cpu format)'); + }); }); describe('queueResourceColumnFormatter', () => { diff --git a/src/app/utils/common.util.ts b/src/app/utils/common.util.ts index 2a0d54ce..63a088ef 100644 --- a/src/app/utils/common.util.ts +++ b/src/app/utils/common.util.ts @@ -88,8 +88,15 @@ export class CommonUtil { if (value === 'n/a') { return 'Memory: n/a'; } - let memory = value.split('%')[0] + '%'; - return CommonUtil.queueResourceColumnFormatter(memory); + //Memory: 4%, CPU: 2% + const memoryRegex = /Memory: ([0-9]|[1-9][0-9]|100)%/; + const match = value.match(memoryRegex); + + if(match){ + return CommonUtil.queueResourceColumnFormatter(match[0]); + } else { + return 'Memory: n/a (wrong memory format)'; + } } static absoluteUsedCPUColumnFormatter(value: string | undefined): string { @@ -99,10 +106,15 @@ export class CommonUtil { if (value === 'n/a') { return 'CPU: n/a'; } - let cpu = value.split('%')[1] + '%'; - cpu = cpu.replace(',', ''); - - return CommonUtil.queueResourceColumnFormatter(cpu); + //Memory: 4%, CPU: 2% + const cpuRegex = /CPU: ([0-9]|[1-9][0-9]|100)%/; + const match = value.match(cpuRegex); + + if(match){ + return CommonUtil.queueResourceColumnFormatter(match[0]); + } else { + return 'CPU: n/a (wrong cpu format)'; + } } static resourceColumnFormatter(value: string): string {