This is an automated email from the ASF dual-hosted git repository.

ccondit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-web.git


The following commit(s) were added to refs/heads/master by this push:
     new 048f46c  [YUNIKORN-2354] Add detailed queue information to new queue 
view (#197)
048f46c is described below

commit 048f46cc711a510ec7e3518f66ffbb15b15809c5
Author: DouPache <[email protected]>
AuthorDate: Tue Sep 24 16:59:36 2024 -0500

    [YUNIKORN-2354] Add detailed queue information to new queue view (#197)
    
    Closes: #197
    
    Signed-off-by: Craig Condit <[email protected]>
---
 json-db.json                                       | 21 +++++
 .../components/queue-v2/queues-v2.component.html   | 95 +++++++++++++++++++++-
 .../components/queue-v2/queues-v2.component.scss   | 93 ++++++++++++++++++++-
 src/app/components/queue-v2/queues-v2.component.ts | 69 ++++++++++++++--
 .../queues-view/queues-view.component.ts           |  2 +-
 src/app/utils/common.util.spec.ts                  | 44 ++++++++++
 src/app/utils/common.util.ts                       | 37 +++++++++
 7 files changed, 352 insertions(+), 9 deletions(-)

diff --git a/json-db.json b/json-db.json
index c95a332..ae20745 100644
--- a/json-db.json
+++ b/json-db.json
@@ -61,6 +61,27 @@
       }
     },
     "children": [
+      {
+        "queuename": "root.inactive",
+        "status": "Stopped",
+        "partition": "",
+        "pendingResource": {},
+        "maxResource": {},
+        "guaranteedResource": {},
+        "allocatedResource": {},
+        "preemptingResource": {},
+        "isLeaf": true,
+        "isManaged": true,
+        "properties": {},
+        "parent": "root",
+        "template": null,
+        "children": [],
+        "absUsedCapacity": {},
+        "maxRunningApps": 0,
+        "runningApps": 0,
+        "currentPriority": -2147483648,
+        "allocatingAcceptedApps": []
+      },
       {
         "queuename": "root.b",
         "status": "Active",
diff --git a/src/app/components/queue-v2/queues-v2.component.html 
b/src/app/components/queue-v2/queues-v2.component.html
index c3e5153..d1397ed 100644
--- a/src/app/components/queue-v2/queues-v2.component.html
+++ b/src/app/components/queue-v2/queues-v2.component.html
@@ -30,7 +30,100 @@
         </div>
       </div>
       <div class="body">
-        <div class="visualize-area">
+        <div class="content-wrapper">
+          <div class="visualize-area">
+          </div>
+
+          <div class="additional-info-element">
+            <!-- additional info header  -->
+            <div class="additional-info-header">
+              <strong>Queue Info</strong>
+            </div>
+            <!-- additional info content  -->
+            <div class="additional-info-content">
+              <div class="info-section">
+
+                <div class="info-block">
+                  <div class="info-block-header">Name:</div>
+                  <div class="info-block-text">
+                    {{ seletedInfo?.queueName }} 
+                  </div>
+                  <div class="info-block-text queue-status"
+                    [innerHTML]="showQueueStats(seletedInfo?.status)">
+                  </div>
+                </div>
+
+                <div class="info-block">
+                  <div class="info-block-header">Allocated:</div>
+                  <div 
+                    class="allocated-block" 
+                    
[innerHTML]="resourceValueFormatter(seletedInfo?.allocatedResource)">
+                  </div>
+                </div>
+
+                <div class="info-block">
+                  <div class="info-block-header">Pending:</div>
+                  <div 
+                    class="pending-block"
+                    
[innerHTML]="resourceValueFormatter(seletedInfo?.pendingResource)">
+                  </div>
+                </div>
+
+                <div class="info-block">
+                  <div class="info-block-header">Max:</div>
+                  <div 
+                    class="max-block"
+                    
[innerHTML]="resourceValueFormatter(seletedInfo?.maxResource)">
+                  </div>
+                </div>
+
+                <div class="info-block">
+                  <div class="info-block-header">Guaranteed:</div>
+                  <div 
+                    class="guaranteed-block"
+                    
[innerHTML]="resourceValueFormatter(seletedInfo?.guaranteedResource)">
+                  </div>
+                </div>
+
+                <div class="info-block">
+                  <div class="info-block-header">Absolute Used Capacity:</div>
+                  <div class="capacity-block">
+                      <div class="capacity-icon">
+                          <!-- SVG for CpuIcon -->
+                          <svg xmlns="http://www.w3.org/2000/svg"; width="24" 
height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" strokeWidth="2" 
strokeLinecap="round" strokeLinejoin="round">
+                              <rect width="16" height="16" x="4" y="4" rx="2" 
/>
+                              <rect width="6" height="6" x="9" y="9" rx="1" />
+                              <path d="M15 2v2" />
+                              <path d="M15 20v2" />
+                              <path d="M2 15h2" />
+                              <path d="M2 9h2" />
+                              <path d="M20 15h2" />
+                              <path d="M20 9h2" />
+                              <path d="M9 2v2" />
+                              <path d="M9 20v2" />
+                          </svg>
+                          <div 
[innerHTML]="memoryValueFormatter(seletedInfo?.absoluteUsedCapacity)"></div>
+                      </div>
+                      <div class="capacity-icon">
+                          <!-- SVG for MemoryStickIcon -->
+                          <svg xmlns="http://www.w3.org/2000/svg"; width="24" 
height="24" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" strokeWidth="2" 
strokeLinecap="round" strokeLinejoin="round">
+                              <path d="M6 19v-3" />
+                              <path d="M10 19v-3" />
+                              <path d="M14 19v-3" />
+                              <path d="M18 19v-3" />
+                              <path d="M8 11V9" />
+                              <path d="M16 11V9" />
+                              <path d="M12 11V9" />
+                              <path d="M2 15h20" />
+                              <path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 
2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 
0-3.837Z" />
+                          </svg>
+                          <div 
[innerHTML]="cpuValueFormatter(seletedInfo?.absoluteUsedCapacity)"></div>
+                      </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
diff --git a/src/app/components/queue-v2/queues-v2.component.scss 
b/src/app/components/queue-v2/queues-v2.component.scss
index 0323d73..8f0f1ef 100644
--- a/src/app/components/queue-v2/queues-v2.component.scss
+++ b/src/app/components/queue-v2/queues-v2.component.scss
@@ -79,6 +79,10 @@
       background-color: #8090a5;
   }
 
+  .content-wrapper {
+    display: flex;
+  }
+
   .visualize-area {
     display: flex;
     flex-direction: column;
@@ -92,4 +96,91 @@
     background-color: #f9fafb; 
     gap: 0.2rem;
     padding: 20px;
-  }
\ No newline at end of file
+  }
+
+  .additional-info-element {
+    width: 420px; 
+    height: 600px;  // as same as visualize-area
+    align-items: stretch;
+    background-color: #f3f6f8; 
+    border-radius: 0.5rem;
+    border: 1px groove #1a1a1b;
+    display: none;
+    overflow: scroll;
+  }
+
+  .additional-info-header {
+    color: #010407;
+    font-size: 1.25rem;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    padding-left: 10px;
+  }
+
+  .info-section {
+    background-color: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+    margin-left: 16px;
+    width: 90%;
+  }
+
+  .info-block {
+    width: 90%;
+    border-bottom: 1px solid #eaeaea;
+    padding: 10px;
+  }
+
+  .queue-status {
+    color: #e13020;
+    font-size: 10px;
+  } 
+
+  .info-block:last-child {
+    border-bottom: none;
+  }
+
+  .info-block-header {
+    font-size: 16px;
+    color: #4a4a4a;
+    font-weight: bold;
+  }
+
+  .info-block-text {
+    font-size: 14px;
+    display:inline;
+  }
+
+  .allocated-block, .capacity-block {
+    font-size: 13px;
+    background-color: #f9f9f9;
+    border-radius: 8px;
+    padding: 4px;
+  }
+
+  .pending-block {
+    font-size: 13px;
+    background-color: #ffffe0;
+    border-radius: 8px;
+    padding: 4px;
+  }
+
+  .max-block {
+      font-size: 13px;
+      background-color: #e0ffe0; /* Light green background */
+      border-radius: 8px;
+      padding: 4px;
+  }
+
+  .guaranteed-block {
+    font-size: 13px;
+    background-color: #d8f3fa; /* Light green background */
+    border-radius: 8px;
+    padding: 4px;
+  }
+
+  .capacity-icon{
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
diff --git a/src/app/components/queue-v2/queues-v2.component.ts 
b/src/app/components/queue-v2/queues-v2.component.ts
index c9f8d27..1020e4f 100644
--- a/src/app/components/queue-v2/queues-v2.component.ts
+++ b/src/app/components/queue-v2/queues-v2.component.ts
@@ -28,6 +28,7 @@ import * as d3hierarchy from "d3-hierarchy";
 import * as d3flextree from "d3-flextree";
 import * as d3zoom from "d3-zoom";
 import { transition } from 'd3-transition'; // we need to import transition 
even if we don't use it explicitly
+import { CommonUtil } from '@app/utils/common.util';
 
 export interface TreeNode {
   name: string;
@@ -43,6 +44,10 @@ export interface TreeNode {
 
 export class QueueV2Component implements OnInit {
   rootQueue: QueueInfo | null = null;
+  seletedInfo: QueueInfo | null = null;
+  resourceValueFormatter = CommonUtil.queueResourceColumnFormatter;
+  memoryValueFormatter = CommonUtil.absoluteUsedMemoryColumnFormatter;
+  cpuValueFormatter = CommonUtil.absoluteUsedCPUColumnFormatter;
 
   constructor(
     private scheduler: SchedulerService,
@@ -68,7 +73,7 @@ export class QueueV2Component implements OnInit {
       .subscribe((data) => {
         if (data && data.rootQueue) {
           this.rootQueue = data.rootQueue;
-          queueVisualization(this.rootQueue as QueueInfo)
+          queueVisualization(this.rootQueue as QueueInfo, this)
           setTimeout(() => this.adjustToScreen(),1000) // since the 
ngAfterViewInit hook is not working, we used setTimeout instead
         }
       });
@@ -78,10 +83,22 @@ export class QueueV2Component implements OnInit {
     const fitButton = document.getElementById('fitButton');
     fitButton?.click(); 
   }
+
+  showQueueStats(status: string | undefined) {
+    console.log('sssss', status)
+    if(status !== 'Active'){
+      return '[Inactive]';
+    } else{
+      return null; 
+    }
+  }
 }
 
-function queueVisualization(rawData : QueueInfo){
+function queueVisualization(rawData : QueueInfo , componentInstance: 
QueueV2Component){
   let numberOfNode = 0;
+  let isShowingDetails = false;
+  let selectedNode: any = null;
+
   const duration = 750;
 
   const svg = select('.visualize-area').append('svg')
@@ -190,18 +207,21 @@ function queueVisualization(rawData : QueueInfo){
           .attr("stroke", "white")
           .attr("stroke-width", 2) 
           .attr("rx", 10)
-          .attr("ry", 10);
+          .attr("ry", 10)
+          .attr("class", "cardMain");
   
         group.append("rect")
           .attr("width", 300)
           .attr("height", 30)
-          .attr("fill", "#d4eaf7");
+          .attr("fill", "#d4eaf7")
+          .attr("class", "cardTop");
   
         group.append("rect")
           .attr("y", 30)
           .attr("width", 300)
           .attr("height", 60)
-          .attr("fill", "white");
+          .attr("fill", "white")
+          .attr("class", "cardMiddle");
   
         group.append("rect")
           .attr("y", 90)
@@ -232,7 +252,10 @@ function queueVisualization(rawData : QueueInfo){
           .attr("stroke", "black") 
           .attr("stroke-width", 1)
           .style("visibility", "hidden")
-          .on('click', click);
+          .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) 
@@ -248,6 +271,40 @@ function queueVisualization(rawData : QueueInfo){
           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)
+
+            select(selectedNode).select('.cardTop').attr("fill", "#d4eaf7")
+          }
+
+          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")
+
+            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(this).select('.cardTop').attr("fill", "#d4eaf7")
+
+            select(".additional-info-element").style("display", "none");
+          }
+
+          adjustVisulizeArea(duration)
+        });
       
         // Hide the circle and '+' text when the mouse leaves the node
         group.on("mouseout", function() {
diff --git a/src/app/components/queues-view/queues-view.component.ts 
b/src/app/components/queues-view/queues-view.component.ts
index 253b37f..36d16f8 100644
--- a/src/app/components/queues-view/queues-view.component.ts
+++ b/src/app/components/queues-view/queues-view.component.ts
@@ -64,7 +64,7 @@ export class QueuesViewComponent implements OnInit {
     { level: 'level_07', next: 'level_08' },
     { level: 'level_08', next: 'level_09' },
   ];
-  resourceValueFormatter = CommonUtil.resourceColumnFormatter;
+  resourceValueFormatter = CommonUtil.queueResourceColumnFormatter;
 
   constructor(
     private scheduler: SchedulerService,
diff --git a/src/app/utils/common.util.spec.ts 
b/src/app/utils/common.util.spec.ts
index 831e057..076be59 100644
--- a/src/app/utils/common.util.spec.ts
+++ b/src/app/utils/common.util.spec.ts
@@ -58,4 +58,48 @@ describe('CommonUtil', () => {
       
expect(CommonUtil.formatOtherResource(inputs[index].toString())).toEqual(expected[index]);
     }
   });
+
+  describe('checkin absoluteUsedMemoryColumnFormatter method result', () => {
+    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('<strong>Memory:</strong>
 n/a');
+    });
+
+    it('should format memory percentage correctly', () => {
+      expect(CommonUtil.absoluteUsedMemoryColumnFormatter('Memory: 
40%')).toBe('<strong>Memory:</strong> 40%');
+    });  
+  });
+
+  describe('checkin absoluteUsedCPUColumnFormatter method result', () => {
+    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.absoluteUsedCPUColumnFormatter('n/a')).toBe('<strong>CPU:</strong>
 n/a');
+    });
+
+    it('should format memory percentage correctly', () => {
+      expect(CommonUtil.absoluteUsedMemoryColumnFormatter('CPU: 
60%')).toBe('<strong>CPU:</strong> 60%');
+    });  
+  });
+
+  describe('queueResourceColumnFormatter', () => {
+    it('should return an empty string for undefined input', () => {
+      expect(CommonUtil.queueResourceColumnFormatter(undefined)).toBe('');
+    });
+  
+    it('should format multiple resources correctly', () => {
+      const input = 'Memory: 50%, CPU: 75%';
+      const expected = '<strong>Memory:</strong> 50%<br/><strong>CPU:</strong> 
75%';
+      expect(CommonUtil.queueResourceColumnFormatter(input)).toBe(expected);
+    });
+  
+    it('should handle "n/a" values correctly', () => {
+      expect(CommonUtil.queueResourceColumnFormatter('n/a')).toBe('n/a');
+    });
+  });
 });
diff --git a/src/app/utils/common.util.ts b/src/app/utils/common.util.ts
index a5183c0..2a0d54c 100644
--- a/src/app/utils/common.util.ts
+++ b/src/app/utils/common.util.ts
@@ -81,10 +81,47 @@ export class CommonUtil {
     return `${toValue.toLocaleString(undefined, { maximumFractionDigits: 2 
})}${unit}`;
   }
 
+  static absoluteUsedMemoryColumnFormatter(value: string | undefined): string {
+    if (!value) {
+      return '';
+    }
+    if (value === 'n/a') {
+      return '<strong>Memory:</strong> n/a';
+    }
+    let memory = value.split('%')[0] + '%';
+    return CommonUtil.queueResourceColumnFormatter(memory);
+  }
+
+  static absoluteUsedCPUColumnFormatter(value: string | undefined): string {
+    if (!value) {
+      return '';
+    }
+    if (value === 'n/a') {
+      return '<strong>CPU:</strong> n/a';
+    }
+    let cpu = value.split('%')[1] + '%';
+    cpu = cpu.replace(',', '');
+    
+    return CommonUtil.queueResourceColumnFormatter(cpu);
+  }
+
   static resourceColumnFormatter(value: string): string {
     return value.split(', ').join('<br/>');
   }
 
+  static queueResourceColumnFormatter(value: string | undefined): string {
+    if (!value) {
+      return '';
+    }
+    return value.split(', ').map(part => {
+      const [key, val] = part.split(': ');
+      if (key === 'n/a') {
+        return key;
+      }
+      return `<strong>${key}:</strong> ${val}`;
+    }).join('<br/>');
+  }
+
   static formatPercent(value: number | string): string {
     const toValue = +value;
     return `${toValue.toFixed(0)}%`;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to