YUNIKORN-250: Display queue properties in queues page (#41)

diff --git a/Makefile b/Makefile
index 8b4fe9c..beaf7d4 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,7 @@
 
 .PHONY: check-license
 check-license:
-	@echo "checking license header"
+	@echo "Checking license header"
 	@licRes=$$(grep -Lr --exclude-dir={node_modules,dist} --include=*.{sh,md,yaml,yml,js,ts,html,js,scss} "Licensed to the Apache Software Foundation" .) ; \
 	if [ -n "$${licRes}" ]; then \
 		echo "following files have incorrect license header:\n$${licRes}" ; \
@@ -69,7 +69,7 @@
 # Build an image based on the production ready version
 .PHONY: image
 image:
-	@echo "building web UI docker image"
+	@echo "Building web UI docker image"
 	@SHA=$$(git rev-parse --short=12 HEAD) ; \
 	docker build -t ${REGISTRY}/yunikorn:web-${VERSION} . \
 	--label "GitRevision=$${SHA}" \
@@ -96,6 +96,6 @@
 
 .PHONY: push_image
 push_image: image
-	@echo "push docker images"
+	@echo "Pushing web UI docker image"
 	echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
 	docker push ${REGISTRY}/yunikorn:web-${VERSION}
diff --git a/jsdb.json b/jsdb.json
index a2c3c9e..c512679 100644
--- a/jsdb.json
+++ b/jsdb.json
@@ -1,7 +1,22 @@
 {
   "clusters": [
     {
-      "clusterName": "kubernetes",
+      "clusterName": "cluster-1",
+      "totalApplications": "1",
+      "failedApplications": "",
+      "pendingApplications": "",
+      "runningApplications": "1",
+      "completedApplications": "",
+      "totalContainers": "2",
+      "failedContainers": "",
+      "pendingContainers": "",
+      "runningContainers": "2",
+      "activeNodes": "1",
+      "totalNodes": "1",
+      "failedNodes": ""
+    },
+    {
+      "clusterName": "cluster-2",
       "totalApplications": "1",
       "failedApplications": "",
       "pendingApplications": "",
@@ -17,56 +32,65 @@
     }
   ],
   "queues": {
-    "partitionname": "[my-kube-cluster]default",
+    "partitionName": "[mycluster]default",
     "capacity": {
-      "capacity": "map[vcore:96000 memory:810085]",
+      "capacity": "map[ephemeral-storage:56453061334 hugepages-1Gi:0 hugepages-2Mi:0 memory:5869 pods:110 vcore:4000]",
       "usedcapacity": "0"
     },
     "nodes": null,
     "queues": {
       "queuename": "root",
-      "status": "RUNNING",
+      "status": "Active",
       "capacities": {
         "capacity": "[memory:1000000 vcore:100000]",
-        "maxcapacity": "[memory:1000000 vcore:100000]",
-        "usedcapacity": "[memory:5908 vcore:4000]",
-        "absusedcapacity": "20"
+        "maxcapacity": "[ephemeral-storage:56453061334 hugepages-1Gi:0 hugepages-2Mi:0 memory:5869 pods:110 vcore:4000]",
+        "usedcapacity": "[memory:1024 vcore:500]",
+        "absusedcapacity": "[memory:17 vcore:12]"
       },
       "queues": [
         {
           "queuename": "advertisement",
-          "status": "RUNNING",
+          "status": "Active",
           "capacities": {
             "capacity": "[memory:500000 vcore:50000]",
-            "maxcapacity": "[memory:500000 vcore:50000]",
-            "usedcapacity": "[memory:5908 vcore:4000]",
-            "absusedcapacity": "20"
+            "maxcapacity": "[memory:800000 vcore:80000]",
+            "usedcapacity": "[]",
+            "absusedcapacity": "[]"
           },
-          "queues": null
+          "queues": null,
+          "properties": {
+            "property2": "value2",
+            "someProperty": "someValue"
+          }
         },
         {
           "queuename": "search",
-          "status": "RUNNING",
+          "status": "Active",
           "capacities": {
             "capacity": "[memory:400000 vcore:40000]",
-            "maxcapacity": "[memory:400000 vcore:40000]",
+            "maxcapacity": "[memory:600000 vcore:60000]",
             "usedcapacity": "[]",
-            "absusedcapacity": "20"
+            "absusedcapacity": "[]"
           },
-          "queues": null
+          "queues": null,
+          "properties": {}
         },
         {
           "queuename": "sandbox",
-          "status": "RUNNING",
+          "status": "Active",
           "capacities": {
             "capacity": "[memory:100000 vcore:10000]",
-            "maxcapacity": "[vcore:10000 memory:100000]",
-            "usedcapacity": "[]",
-            "absusedcapacity": "20"
+            "maxcapacity": "[memory:100000 vcore:10000]",
+            "usedcapacity": "[memory:1024 vcore:500]",
+            "absusedcapacity": "[memory:1 vcore:5]"
           },
-          "queues": null
+          "queues": null,
+          "properties": {}
         }
-      ]
+      ],
+      "properties": {
+        "property1": "value1"
+      }
     }
   },
   "apps": [
@@ -171,15 +195,15 @@
     },
     {
       "timestamp": 1585238443804046000,
-      "totalApplications": "1"
+      "totalApplications": "2"
     },
     {
       "timestamp": 1585238503806253000,
-      "totalApplications": "2"
+      "totalApplications": "1"
     },
     {
       "timestamp": 1585238563807452000,
-      "totalApplications": "2"
+      "totalApplications": "3"
     }
   ],
   "containerHistory": [
@@ -197,15 +221,15 @@
     },
     {
       "timestamp": 1585238443804046000,
-      "totalContainers": "1"
+      "totalContainers": "2"
     },
     {
       "timestamp": 1585238503806253000,
-      "totalContainers": "2"
+      "totalContainers": "1"
     },
     {
       "timestamp": 1585238563807452000,
-      "totalContainers": "2"
+      "totalContainers": "3"
     }
   ],
   "nodes": [
diff --git a/src/app/components/queues-view/queues-view.component.html b/src/app/components/queues-view/queues-view.component.html
index bcd5838..72e2f85 100644
--- a/src/app/components/queues-view/queues-view.component.html
+++ b/src/app/components/queues-view/queues-view.component.html
@@ -75,6 +75,10 @@
               <div class="left-item">Absolute Used Capacity:</div>
               <div class="right-item">{{ selectedQueue.absoluteUsedCapacity }}</div>
             </div>
+            <div class="flex-grid item-wrapper" *ngFor="let prop of selectedQueue.queueProperties">
+              <div class="left-item">{{ prop.name }}:</div>
+              <div class="right-item">{{ prop.value }}</div>
+            </div>
           </div>
         </mat-drawer-content>
       </mat-drawer>
diff --git a/src/app/models/queue-info.model.ts b/src/app/models/queue-info.model.ts
index 04615f9..d7b1c76 100644
--- a/src/app/models/queue-info.model.ts
+++ b/src/app/models/queue-info.model.ts
@@ -31,6 +31,12 @@
   isLeafQueue: boolean;
   isExpanded = false;
   isSelected = false;
+  queueProperties: QueuePropertyItem[];
+}
+
+export interface QueuePropertyItem {
+  name: string;
+  value: string;
 }
 
 export interface SchedulerInfo {
diff --git a/src/app/services/scheduler/scheduler.service.ts b/src/app/services/scheduler/scheduler.service.ts
index 186026f..84d846d 100644
--- a/src/app/services/scheduler/scheduler.service.ts
+++ b/src/app/services/scheduler/scheduler.service.ts
@@ -21,7 +21,7 @@
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
-import { QueueInfo } from '@app/models/queue-info.model';
+import { QueueInfo, QueuePropertyItem } from '@app/models/queue-info.model';
 import { EnvconfigService } from '../envconfig/envconfig.service';
 import { ClusterInfo } from '@app/models/cluster-info.model';
 import { CommonUtil } from '@app/utils/common.util';
@@ -45,9 +45,7 @@
 
   public fetchClusterByName(clusterName: string): Observable<ClusterInfo> {
     return this.fetchClusterList().pipe(
-      map(data => {
-        return data.find(obj => obj.clusterName === clusterName);
-      })
+      map(data => data.find(obj => obj.clusterName === clusterName))
     );
   }
 
@@ -63,6 +61,7 @@
           rootQueue.children = null;
           rootQueue.isLeafQueue = false;
           this.fillQueueCapacities(rootQueueData, rootQueue);
+          this.fillQueueProperties(rootQueueData, rootQueue);
           rootQueue = this.generateQueuesTree(rootQueueData, rootQueue);
         }
         const partitionName = data['partitionname'] || '';
@@ -216,14 +215,15 @@
   private generateQueuesTree(data: any, currentQueue: QueueInfo) {
     if (data && data.queues && data.queues.length > 0) {
       const chilrenQs = [];
-      data.queues.forEach(queue => {
+      data.queues.forEach(queueData => {
         const childQueue = new QueueInfo();
-        childQueue.queueName = '' + queue.queuename;
-        childQueue.state = queue.status || 'RUNNING';
+        childQueue.queueName = '' + queueData.queuename;
+        childQueue.state = queueData.status || 'RUNNING';
         childQueue.parentQueue = currentQueue ? currentQueue : null;
-        this.fillQueueCapacities(queue, childQueue);
+        this.fillQueueCapacities(queueData, childQueue);
+        this.fillQueueProperties(queueData, childQueue);
         chilrenQs.push(childQueue);
-        return this.generateQueuesTree(queue, childQueue);
+        return this.generateQueuesTree(queueData, childQueue);
       });
       currentQueue.children = chilrenQs;
       currentQueue.isLeafQueue = false;
@@ -239,15 +239,27 @@
     const maxCap = data['capacities']['maxcapacity'] as string;
     const absUsedCapacity = data['capacities']['absusedcapacity'] as string;
 
-    const configCapResources = this.splitCapacity(configCap, NOT_AVAILABLE);
-    const usedCapResources = this.splitCapacity(usedCap, NOT_AVAILABLE);
-    const maxCapResources = this.splitCapacity(maxCap, NOT_AVAILABLE);
-    const absUsedCapacityResources = this.splitCapacity(absUsedCapacity, NOT_AVAILABLE);
+    queue.capacity = this.formatCapacity(this.splitCapacity(configCap, NOT_AVAILABLE));
+    queue.maxCapacity = this.formatCapacity(this.splitCapacity(maxCap, NOT_AVAILABLE));
+    queue.usedCapacity = this.formatCapacity(this.splitCapacity(usedCap, NOT_AVAILABLE));
+    queue.absoluteUsedCapacity = this.formatAbsCapacity(
+      this.splitCapacity(absUsedCapacity, NOT_AVAILABLE)
+    );
+  }
 
-    queue.capacity = this.formatCapacity(configCapResources);
-    queue.maxCapacity = this.formatCapacity(maxCapResources);
-    queue.usedCapacity = this.formatCapacity(usedCapResources);
-    queue.absoluteUsedCapacity = this.formatAbsCapacity(absUsedCapacityResources);
+  private fillQueueProperties(data: any, queue: QueueInfo) {
+    if (data.properties && !CommonUtil.isEmpty(data.properties)) {
+      const dataProps = Object.entries<string>(data.properties);
+
+      queue.queueProperties = dataProps.map(prop => {
+        return {
+          name: prop[0],
+          value: prop[1]
+        } as QueuePropertyItem;
+      });
+    } else {
+      queue.queueProperties = [];
+    }
   }
 
   private splitCapacity(capacity: string = '', defaultValue: string): ResourceInfo {
@@ -279,11 +291,15 @@
   private formatCapacity(resourceInfo: ResourceInfo) {
     const formatted = [];
     if (resourceInfo.memory !== NOT_AVAILABLE) {
-      formatted.push(`[memory: ${CommonUtil.formatMemory(+resourceInfo.memory)}`);
+      formatted.push(`[memory: ${CommonUtil.formatMemory(resourceInfo.memory)}`);
     } else {
       formatted.push(`[memory: ${resourceInfo.memory}`);
     }
-    formatted.push(`vcore: ${resourceInfo.vcore}]`);
+    if (resourceInfo.vcore !== NOT_AVAILABLE) {
+      formatted.push(`vcore: ${CommonUtil.formatCount(resourceInfo.vcore)}]`);
+    } else {
+      formatted.push(`vcore: ${resourceInfo.vcore}]`);
+    }
     return formatted.join(', ');
   }
 
diff --git a/src/app/utils/common.util.ts b/src/app/utils/common.util.ts
index 299c41d..c2d11c6 100644
--- a/src/app/utils/common.util.ts
+++ b/src/app/utils/common.util.ts
@@ -17,39 +17,52 @@
  */
 
 export class CommonUtil {
-  static createUniqId(prefix?: string) {
+  static createUniqId(prefix?: string): string {
     const uniqid = Math.random()
       .toString(36)
       .substr(2);
+
     if (prefix) {
       return prefix + uniqid;
     }
+
     return uniqid;
   }
 
-  static formatMemory(value: number) {
-    let toUnit = 'MB';
-    let toValue = value;
-    // if (toValue / 1024 >= 0.9) {
-    //     toValue = toValue / 1024;
-    //     toUnit = 'KB';
-    // }
-    // if (toValue / 1024 >= 0.9) {
-    //     toValue = toValue / 1024;
-    //     toUnit = 'MB';
-    // }
+  static formatMemory(value: number | string): string {
+    let unit = 'MB';
+    let toValue = +value;
+
     if (toValue / 1024 >= 0.9) {
       toValue = toValue / 1024;
-      toUnit = 'GB';
+      unit = 'GB';
     }
+
     if (toValue / 1024 >= 0.9) {
       toValue = toValue / 1024;
-      toUnit = 'TB';
+      unit = 'TB';
     }
+
     if (toValue / 1024 >= 0.9) {
       toValue = toValue / 1024;
-      toUnit = 'PB';
+      unit = 'PB';
     }
-    return toValue.toFixed(1) + ' ' + toUnit;
+
+    return `${toValue.toFixed(1)} ${unit}`;
+  }
+
+  static isEmpty(arg: object | any[]): boolean {
+    return Object.keys(arg).length === 0;
+  }
+
+  static formatCount(value: number | string): string {
+    const unit = 'K';
+    const toValue = +value;
+
+    if (toValue >= 1000) {
+      return `${(toValue / 1000).toFixed(1)} ${unit}`;
+    }
+
+    return toValue.toString();
   }
 }