[YUNIKORN-2491] Persist queue and partition selection and prompt user to pick queue on Applications page
 (#180)

- When a user lands on the applications page, queue selection is automatically focused and waits for user to select the queue
- Selected queue,partition pair is stored in localStorage

Closes: #180

Signed-off-by: Yu-Lin Chen <chenyulin0719@apache.org>
diff --git a/src/app/components/apps-view/apps-view.component.html b/src/app/components/apps-view/apps-view.component.html
index 78f408a..8cf5896 100644
--- a/src/app/components/apps-view/apps-view.component.html
+++ b/src/app/components/apps-view/apps-view.component.html
@@ -31,7 +31,7 @@
     <div class="dropdown-wrapper">
       <label class="dropdown-label">Queue: </label>
       <mat-form-field>
-        <mat-select [(value)]="leafQueueSelected" (selectionChange)="onQueueSelectionChanged($event)">
+        <mat-select [(value)]="leafQueueSelected" (selectionChange)="onQueueSelectionChanged($event)" #queueSelect>
           <mat-option *ngFor="let queue of leafQueueList" [value]="queue.value">
             {{ queue.name }}
           </mat-option>
diff --git a/src/app/components/apps-view/apps-view.component.ts b/src/app/components/apps-view/apps-view.component.ts
index 5bbcc96..edbf0bb 100644
--- a/src/app/components/apps-view/apps-view.component.ts
+++ b/src/app/components/apps-view/apps-view.component.ts
@@ -21,7 +21,7 @@
 import { MatPaginator } from '@angular/material/paginator';
 import { MatTableDataSource } from '@angular/material/table';
 import { MatSort } from '@angular/material/sort';
-import { MatSelectChange } from '@angular/material/select';
+import { MatSelectChange, MatSelect } from '@angular/material/select';
 import { finalize, debounceTime, distinctUntilChanged } from 'rxjs/operators';
 import { NgxSpinnerService } from 'ngx-spinner';
 import { fromEvent } from 'rxjs';
@@ -46,6 +46,7 @@
   @ViewChild('appSort', { static: true }) appSort!: MatSort;
   @ViewChild('allocSort', { static: true }) allocSort!: MatSort;
   @ViewChild('searchInput', { static: true }) searchInput!: ElementRef;
+  @ViewChild('queueSelect', { static: false }) queueSelect!: MatSelect;
 
   appDataSource = new MatTableDataSource<AppInfo>([]);
   appColumnDef: ColumnDef[] = [];
@@ -62,7 +63,7 @@
   partitionSelected = '';
   leafQueueList: DropdownItem[] = [];
   leafQueueSelected = '';
-  
+
   detailToggle: boolean = false;
 
   constructor(
@@ -135,7 +136,7 @@
             this.partitionList.push(new PartitionInfo(part.name, part.name));
           });
 
-          this.partitionSelected = list[0].name;
+          this.partitionSelected = CommonUtil.getStoredPartition(list[0].name);
           this.fetchQueuesForPartition(this.partitionSelected);
         } else {
           this.partitionList = [new PartitionInfo('-- Select --', '')];
@@ -143,6 +144,7 @@
           this.leafQueueList = [new DropdownItem('-- Select --', '')];
           this.leafQueueSelected = '';
           this.appDataSource.data = [];
+          this.clearQueueSelection();
         }
       });
   }
@@ -161,14 +163,37 @@
         if (data && data.rootQueue) {
           const leafQueueList = this.generateLeafQueueList(data.rootQueue);
           this.leafQueueList = [new DropdownItem('-- Select --', ''), ...leafQueueList];
-          this.leafQueueSelected = '';
           this.fetchApplicationsUsingQueryParams();
+          this.setDefaultQueue(leafQueueList);
         } else {
           this.leafQueueList = [new DropdownItem('-- Select --', '')];
         }
       });
   }
 
+  setDefaultQueue(queueList: DropdownItem[]): void {
+    const storedPartitionAndQueue = localStorage.getItem('selectedPartitionAndQueue');
+
+    if (!storedPartitionAndQueue || storedPartitionAndQueue.indexOf(':') < 0) {
+      setTimeout(() => this.openQueueSelection(), 0);
+      return;
+    }
+
+    const [storedPartition, storedQueue] = storedPartitionAndQueue.split(':');
+    if (this.partitionSelected !== storedPartition) return;
+
+    const storedQueueDropdownItem = queueList.find((queue) => queue.value === storedQueue);
+    if (storedQueueDropdownItem) {
+      this.leafQueueSelected = storedQueueDropdownItem.value;
+      this.fetchAppListForPartitionAndQueue(this.partitionSelected, this.leafQueueSelected);
+      return;
+    } else {
+      this.leafQueueSelected = '';
+      this.appDataSource.data = [];
+      setTimeout(() => this.openQueueSelection(), 0); // Allows render to finish and then opens the queue select dropdown
+    }
+  }
+
   generateLeafQueueList(rootQueue: QueueInfo, list: DropdownItem[] = []): DropdownItem[] {
     if (rootQueue && rootQueue.isLeaf) {
       list.push(new DropdownItem(rootQueue.queueName, rootQueue.queueName));
@@ -205,6 +230,7 @@
       this.partitionSelected = partitionName;
       this.leafQueueSelected = queueName;
       this.fetchAppListForPartitionAndQueue(partitionName, queueName);
+      CommonUtil.setStoredQueueAndPartition(partitionName, queueName);
     }
 
     this.router.navigate([], {
@@ -284,6 +310,7 @@
       this.partitionSelected = selected.value;
       this.appDataSource.data = [];
       this.removeRowSelection();
+      this.clearQueueSelection();
       this.fetchQueuesForPartition(this.partitionSelected);
     } else {
       this.searchText = '';
@@ -291,6 +318,7 @@
       this.leafQueueSelected = '';
       this.appDataSource.data = [];
       this.removeRowSelection();
+      this.clearQueueSelection();
     }
   }
 
@@ -301,35 +329,51 @@
       this.appDataSource.data = [];
       this.removeRowSelection();
       this.fetchAppListForPartitionAndQueue(this.partitionSelected, this.leafQueueSelected);
+      CommonUtil.setStoredQueueAndPartition(this.partitionSelected, this.leafQueueSelected);
     } else {
       this.searchText = '';
       this.leafQueueSelected = '';
       this.appDataSource.data = [];
       this.removeRowSelection();
+      this.clearQueueSelection();
     }
   }
 
-  formatResources(colValue:string):string[]{
-    const arr:string[]=colValue.split("<br/>")
+  formatResources(colValue: string): string[] {
+    const arr: string[] = colValue.split('<br/>');
     // Check if there are "cpu" or "Memory" elements in the array
-    const hasCpu = arr.some((item) => item.toLowerCase().includes("cpu"));
-    const hasMemory = arr.some((item) => item.toLowerCase().includes("memory"));
+    const hasCpu = arr.some((item) => item.toLowerCase().includes('cpu'));
+    const hasMemory = arr.some((item) => item.toLowerCase().includes('memory'));
     if (!hasCpu) {
-      arr.unshift("CPU: n/a");
+      arr.unshift('CPU: n/a');
     }
     if (!hasMemory) {
-      arr.unshift("Memory: n/a");
+      arr.unshift('Memory: n/a');
     }
 
     // Concatenate the two arrays, with "cpu" and "Memory" elements first
-    const cpuAndMemoryElements = arr.filter((item) => item.toLowerCase().includes("CPU") || item.toLowerCase().includes("Memory"));
-    const otherElements = arr.filter((item) => !item.toLowerCase().includes("CPU") && !item.toLowerCase().includes("Memory"));
+    const cpuAndMemoryElements = arr.filter(
+      (item) => item.toLowerCase().includes('CPU') || item.toLowerCase().includes('Memory')
+    );
+    const otherElements = arr.filter(
+      (item) => !item.toLowerCase().includes('CPU') && !item.toLowerCase().includes('Memory')
+    );
     const result = cpuAndMemoryElements.concat(otherElements);
 
     return result;
   }
 
-  toggle(){
+  clearQueueSelection() {
+    CommonUtil.setStoredQueueAndPartition('');
+    this.leafQueueSelected = '';
+    this.openQueueSelection();
+  }
+
+  openQueueSelection() {
+    this.queueSelect.open();
+  }
+
+  toggle() {
     this.detailToggle = !this.detailToggle;
   }
 }
diff --git a/src/app/components/nodes-view/nodes-view.component.ts b/src/app/components/nodes-view/nodes-view.component.ts
index 9bcf58a..e4c4f51 100644
--- a/src/app/components/nodes-view/nodes-view.component.ts
+++ b/src/app/components/nodes-view/nodes-view.component.ts
@@ -39,9 +39,8 @@
 export class NodesViewComponent implements OnInit {
   @ViewChild('nodesViewMatPaginator', { static: true }) nodePaginator!: MatPaginator;
   @ViewChild('allocationMatPaginator', { static: true }) allocPaginator!: MatPaginator;
-  @ViewChild('nodeSort', {static: true }) nodeSort!: MatSort;
-  @ViewChild('allocSort', {static: true }) allocSort!: MatSort;
-
+  @ViewChild('nodeSort', { static: true }) nodeSort!: MatSort;
+  @ViewChild('allocSort', { static: true }) allocSort!: MatSort;
 
   nodeDataSource = new MatTableDataSource<NodeInfo>([]);
   nodeColumnDef: ColumnDef[] = [];
@@ -58,7 +57,10 @@
   detailToggle: boolean = false;
   filterValue: string = '';
 
-  constructor(private scheduler: SchedulerService, private spinner: NgxSpinnerService) {}
+  constructor(
+    private scheduler: SchedulerService,
+    private spinner: NgxSpinnerService
+  ) {}
 
   ngOnInit() {
     this.nodeDataSource.paginator = this.nodePaginator;
@@ -118,12 +120,13 @@
             this.partitionList.push(new PartitionInfo(part.name, part.name));
           });
 
-          this.partitionSelected = list[0].name;
+          this.partitionSelected = CommonUtil.getStoredPartition(list[0].name);
           this.fetchNodeListForPartition(this.partitionSelected);
         } else {
           this.partitionList = [new PartitionInfo('-- Select --', '')];
           this.partitionSelected = '';
           this.nodeDataSource.data = [];
+          CommonUtil.setStoredQueueAndPartition('');
         }
       });
   }
@@ -192,95 +195,103 @@
     return this.allocDataSource.data && this.allocDataSource.data.length === 0;
   }
 
-  formatColumn(){
-    if(this.nodeDataSource.data.length===0){
+  formatColumn() {
+    if (this.nodeDataSource.data.length === 0) {
       return;
     }
-    this.nodeColumnIds.forEach((colId)=>{
-      if (colId==='indicatorIcon'){
+    this.nodeColumnIds.forEach((colId) => {
+      if (colId === 'indicatorIcon') {
         return;
       }
 
       // Verify whether all cells in the column are empty.
-      let isEmpty:boolean = true;
+      let isEmpty: boolean = true;
       Object.values(this.nodeDataSource.data).forEach((node) => {
-        Object.entries(node).forEach(entry => {
+        Object.entries(node).forEach((entry) => {
           const [key, value] = entry;
-          if (key===colId && !(value==='' || value==='n/a')){
-            isEmpty=false;
+          if (key === colId && !(value === '' || value === 'n/a')) {
+            isEmpty = false;
           }
         });
       });
-      
-      if (isEmpty){
-        this.nodeColumnIds = this.nodeColumnIds.filter(el => el!==colId);
-        this.nodeColumnIds = this.nodeColumnIds.filter(colId => colId!=="attributes");
+
+      if (isEmpty) {
+        this.nodeColumnIds = this.nodeColumnIds.filter((el) => el !== colId);
+        this.nodeColumnIds = this.nodeColumnIds.filter((colId) => colId !== 'attributes');
       }
-    })
+    });
   }
 
   onPartitionSelectionChanged(selected: MatSelectChange) {
     this.partitionSelected = selected.value;
     this.clearRowSelection();
     this.fetchNodeListForPartition(this.partitionSelected);
+    CommonUtil.setStoredQueueAndPartition(this.partitionSelected);
   }
 
-  formatResources(colValue:string):string[]{
-    const arr:string[]=colValue.split("<br/>");
+  formatResources(colValue: string): string[] {
+    const arr: string[] = colValue.split('<br/>');
     // Check if there are "cpu" or "Memory" elements in the array
-    const hasCpu = arr.some((item) => item.toLowerCase().includes("cpu"));
-    const hasMemory = arr.some((item) => item.toLowerCase().includes("memory"));
+    const hasCpu = arr.some((item) => item.toLowerCase().includes('cpu'));
+    const hasMemory = arr.some((item) => item.toLowerCase().includes('memory'));
     if (!hasCpu) {
-      arr.unshift("CPU: n/a");
+      arr.unshift('CPU: n/a');
     }
     if (!hasMemory) {
-      arr.unshift("Memory: n/a");
+      arr.unshift('Memory: n/a');
     }
 
     // Concatenate the two arrays, with "cpu" and "Memory" elements first
-    const cpuAndMemoryElements = arr.filter((item) => item.toLowerCase().includes("CPU") || item.toLowerCase().includes("Memory"));
-    const otherElements = arr.filter((item) => !item.toLowerCase().includes("CPU") && !item.toLowerCase().includes("Memory"));
+    const cpuAndMemoryElements = arr.filter(
+      (item) => item.toLowerCase().includes('CPU') || item.toLowerCase().includes('Memory')
+    );
+    const otherElements = arr.filter(
+      (item) => !item.toLowerCase().includes('CPU') && !item.toLowerCase().includes('Memory')
+    );
     const result = cpuAndMemoryElements.concat(otherElements);
 
     return result;
   }
 
-  formatAttribute(attributes:any):string[]{
-    let result:string[]=[];
-    Object.entries(attributes).forEach(entry=>{
+  formatAttribute(attributes: any): string[] {
+    let result: string[] = [];
+    Object.entries(attributes).forEach((entry) => {
       const [key, value] = entry;
-      if (value==="" || key.includes("si")){
-        return
+      if (value === '' || key.includes('si')) {
+        return;
       }
-      result.push(key+':'+value);
-    })
+      result.push(key + ':' + value);
+    });
     return result;
   }
 
-  toggle(){
+  toggle() {
     this.detailToggle = !this.detailToggle;
     this.displayAttribute(this.detailToggle);
   }
 
-  displayAttribute(toggle:boolean) {
-    if (toggle){
+  displayAttribute(toggle: boolean) {
+    if (toggle) {
       this.nodeColumnIds = [
         ...this.nodeColumnIds.slice(0, 1),
-        "attributes",
-        ...this.nodeColumnIds.slice(1)
-    ];
-    }else{
-      this.nodeColumnIds=this.nodeColumnIds.filter(colId => colId!=="attributes");
+        'attributes',
+        ...this.nodeColumnIds.slice(1),
+      ];
+    } else {
+      this.nodeColumnIds = this.nodeColumnIds.filter((colId) => colId !== 'attributes');
     }
   }
 
-  filterPredicate: ((data: NodeInfo, filter: string) => boolean) = (data: NodeInfo, filter: string): boolean => {
+  filterPredicate: (data: NodeInfo, filter: string) => boolean = (
+    data: NodeInfo,
+    filter: string
+  ): boolean => {
     // a deep copy of the NodeInfo with formatted attributes for filtering
     const deepCopy = JSON.parse(JSON.stringify(data));
-    Object.entries(deepCopy.attributes).forEach(entry=>{
+    Object.entries(deepCopy.attributes).forEach((entry) => {
       const [key, value] = entry;
-      deepCopy.attributes[key]= `${key}:${value}`
-    })
+      deepCopy.attributes[key] = `${key}:${value}`;
+    });
     const objectString = JSON.stringify(deepCopy).toLowerCase();
     return objectString.includes(filter);
   };
diff --git a/src/app/components/queues-view/queues-view.component.ts b/src/app/components/queues-view/queues-view.component.ts
index 389f42e..253b37f 100644
--- a/src/app/components/queues-view/queues-view.component.ts
+++ b/src/app/components/queues-view/queues-view.component.ts
@@ -92,12 +92,14 @@
             this.partitionList.push(new PartitionInfo(part.name, part.name));
           });
 
-          this.partitionSelected = list[0].name;
+          this.partitionSelected = CommonUtil.getStoredPartition(list[0].name);
+
           this.fetchSchedulerQueuesForPartition(this.partitionSelected);
         } else {
           this.partitionList = [new PartitionInfo('-- Select --', '')];
           this.partitionSelected = '';
           this.queueList = {};
+          CommonUtil.setStoredQueueAndPartition('');
         }
       });
   }
@@ -208,9 +210,11 @@
       this.partitionSelected = selected.value;
       this.closeQueueDrawer();
       this.fetchSchedulerQueuesForPartition(this.partitionSelected);
+      CommonUtil.setStoredQueueAndPartition(this.partitionSelected);
     } else {
       this.partitionSelected = '';
       this.queueList = {};
+      CommonUtil.setStoredQueueAndPartition('');
     }
   }
 
diff --git a/src/app/utils/common.util.ts b/src/app/utils/common.util.ts
index 98f2a18..c810873 100644
--- a/src/app/utils/common.util.ts
+++ b/src/app/utils/common.util.ts
@@ -115,4 +115,17 @@
       return a.localeCompare(b);  // Other resources will be in lexicographic order
     }
   }
+
+  static getStoredPartition(defaultValue = ''): string {
+    const storedPartition = localStorage.getItem('selectedPartitionAndQueue');
+
+    if (storedPartition && storedPartition.indexOf(':') > 0) return storedPartition.split(':')[0];
+
+    return defaultValue;
+  }
+
+  static setStoredQueueAndPartition(partition: string, queue = '') {
+    if (partition) localStorage.setItem('selectedPartitionAndQueue', `${partition}:${queue}`);
+    else localStorage.removeItem('selectedPartitionAndQueue');
+  }
 }