From 41ed28de6c9d1c129d0f6d10e539c34652616315 Mon Sep 17 00:00:00 2001
From: SP12893678 <36910625+SP12893678@users.noreply.github.com>
Date: Mon, 25 Nov 2024 21:59:57 +0800
Subject: [PATCH] [YUNIKORN-2986] Improve visual in applications/node page
---
package.json | 1 +
pnpm-lock.yaml | 17 +
src/app/app.module.ts | 11 +-
.../apps-view/apps-view.component.html | 340 +++++++++---------
.../apps-view/apps-view.component.scss | 46 +--
.../apps-view/apps-view.component.ts | 90 ++---
.../nodes-view/nodes-view.component.html | 196 +++++-----
.../nodes-view/nodes-view.component.scss | 9 +-
.../nodes-view/nodes-view.component.ts | 11 +-
.../queue-menu-tree.component.html | 57 +++
.../queue-menu-tree.component.scss | 135 +++++++
.../queue-menu-tree.component.ts | 87 +++++
.../search-input/search-input.component.html | 36 ++
.../search-input/search-input.component.scss | 48 +++
.../search-input/search-input.component.ts | 49 +++
src/styles.scss | 37 ++
16 files changed, 789 insertions(+), 381 deletions(-)
create mode 100644 src/app/components/queue-menu-tree/queue-menu-tree.component.html
create mode 100644 src/app/components/queue-menu-tree/queue-menu-tree.component.scss
create mode 100644 src/app/components/queue-menu-tree/queue-menu-tree.component.ts
create mode 100644 src/app/components/search-input/search-input.component.html
create mode 100644 src/app/components/search-input/search-input.component.scss
create mode 100644 src/app/components/search-input/search-input.component.ts
diff --git a/package.json b/package.json
index 5a2dcff3..a43f1a94 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"angular-material-expansion-panel": "^0.7.2",
+ "angular-split": "^18.0.0",
"chart.js": "^4.4.4",
"chartjs-adapter-date-fns": "^3.0.0",
"color": "^4.2.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 49f3be3d..3baca03f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -65,6 +65,9 @@ importers:
angular-material-expansion-panel:
specifier: ^0.7.2
version: 0.7.2
+ angular-split:
+ specifier: ^18.0.0
+ version: 18.0.0(@angular/common@18.2.6(@angular/core@18.2.6(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@18.2.6(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1)
chart.js:
specifier: ^4.4.4
version: 4.4.4
@@ -1972,6 +1975,13 @@ packages:
angular-material-expansion-panel@0.7.2:
resolution: {integrity: sha512-LTmjaSLCRKb8s2QPMaYqYp/9D8pDY4l5GDuvlDG9jTeLpQYyEK4IbcDpGqMkLyNUNk28P02cIyGcMsuUZa5KjA==}
+ angular-split@18.0.0:
+ resolution: {integrity: sha512-vreR7dhwg6ubC3ZZn0vJG9Fb+8Xacf77FRQ/3IdChzsRFya1LxJh/Wk7uBk8q9Xi0pOKBKruZ3OWGjhqvHmETg==}
+ peerDependencies:
+ '@angular/common': '>=18.0.0'
+ '@angular/core': '>=18.0.0'
+ rxjs: '>=7.0.0'
+
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -6918,6 +6928,13 @@ snapshots:
angular-material-expansion-panel@0.7.2: {}
+ angular-split@18.0.0(@angular/common@18.2.6(@angular/core@18.2.6(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1))(@angular/core@18.2.6(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1):
+ dependencies:
+ '@angular/common': 18.2.6(@angular/core@18.2.6(rxjs@7.8.1)(zone.js@0.14.10))(rxjs@7.8.1)
+ '@angular/core': 18.2.6(rxjs@7.8.1)(zone.js@0.14.10)
+ rxjs: 7.8.1
+ tslib: 2.7.0
+
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index dd3bc6e6..90c0dda2 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -22,7 +22,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgxSpinnerModule } from 'ngx-spinner';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { MatSelectModule } from '@angular/material/select';
@@ -64,6 +64,10 @@ import { AppNodeUtilizationsComponent } from '@app/components/app-node-utilizati
import { VerticalBarChartComponent } from '@app/components/vertical-bar-chart/vertical-bar-chart.component';
import { LicensesModalComponent } from '@app/components/licenses-modal/licenses-modal.component';
import { CardComponent } from './components/card/card.component';
+import { QueueMenuTreeComponent } from './components/queue-menu-tree/queue-menu-tree.component';
+import { MatTreeModule } from '@angular/material/tree';
+import { AngularSplitModule } from 'angular-split';
+import { SearchInputComponent } from './components/search-input/search-input.component';
@NgModule({
declarations: [
@@ -87,6 +91,8 @@ import { CardComponent } from './components/card/card.component';
VerticalBarChartComponent,
LicensesModalComponent,
CardComponent,
+ QueueMenuTreeComponent,
+ SearchInputComponent
],
imports: [
BrowserModule,
@@ -113,6 +119,9 @@ import { CardComponent } from './components/card/card.component';
MatExpansionModule,
MatIconModule,
MatDialogModule,
+ MatTreeModule,
+ AngularSplitModule,
+ ReactiveFormsModule
],
providers: [
{
diff --git a/src/app/components/apps-view/apps-view.component.html b/src/app/components/apps-view/apps-view.component.html
index 5e44df06..ddd8d118 100644
--- a/src/app/components/apps-view/apps-view.component.html
+++ b/src/app/components/apps-view/apps-view.component.html
@@ -16,207 +16,191 @@
* limitations under the License.
-->
-
-
-
-
-
+
+
+
+
+ Partition
{{ part.name }}
-
-
-
-
-
-
- {{ queue.name }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ columnDef.colName }}
-
-
- {{ element['formattedSubmissionTime'] }}
-
+
+
+
+
+
+
+
+ {{ columnDef.colName }}
-
- {{ element['formattedlastStateChangeTime'] }}
-
+
+ {{ element['formattedSubmissionTime'] }}
+
-
-
-
-
-
-
-
- -
- {{ resource }}
-
- - =2 && detailToggle">
- {{ resource }}
-
-
-
-
-
-
- {{ element[columnDef.colId] }}
-
-
+
+ {{ element['formattedlastStateChangeTime'] }}
-
+
-
+
+
+
+
+ -
+ {{ resource }}
+
+ - =2 && detailToggle">
+ {{ resource }}
+
+
+
+
+
+
+ {{ element[columnDef.colId] }}
+
-
-
-
-
- {{ element[columnDef.colId] || 'n/a' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- No records found
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ columnDef.colName }}
-
-
- {{ element['priority'] }}
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- -
- {{ resource }}
-
- - =1 && allocationsToggle">
- {{ resource }}
-
+
+ {{ element[columnDef.colId] || 'n/a' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No records found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ columnDef.colName }}
+
+
+ {{
+ element['priority'] }}
+
+
+
+
+
+
+
+
+ -
+ {{ resource }}
+
+ - =1 && allocationsToggle">
+ {{ resource }}
+
+
+
-
-
+
+
+ {{ element[columnDef.colId] }}
+
+
-
- {{ element[columnDef.colId] }}
+
+
+ {{ element[columnDef.colId] ||
+ 'n/a' }}
-
-
+
-
- {{ element[columnDef.colId] || 'n/a' }}
-
-
+
+
+ No records found
+
+
-
-
- No records found
-
-
+
-
+
-
+
+
-
-
+
-
+
+
+
+
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/app/components/apps-view/apps-view.component.scss b/src/app/components/apps-view/apps-view.component.scss
index d6f2f748..82d9cfb7 100644
--- a/src/app/components/apps-view/apps-view.component.scss
+++ b/src/app/components/apps-view/apps-view.component.scss
@@ -17,14 +17,25 @@
*/
@import '~material-design-icons/iconfont/material-icons.css';
+.apps-view {
+ height: 100%;
+ overflow-y: hidden;
+}
+
+as-split-area {
+ padding: 10px 0px;
+}
+
.top-section {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
+ gap: 12px;
.left-side {
+ flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
@@ -69,38 +80,6 @@
transform: translateY(2px);
}
}
-
- .search-wrapper {
- width: 300px;
- right: 20px;
- padding-right: 20px;
-
- input {
- width: calc(100% - 22px);
- color: #333;
- }
-
- .clear-btn {
- outline: none;
- border: none;
- padding: 0 0 0 4px;
- cursor: pointer;
- background: transparent;
-
- i {
- font-size: 18px;
-
- &:hover {
- color: #f44336;
- }
- }
- }
-
- .search-icon {
- margin-left: 4px;
- font-size: 17px;
- }
- }
}
}
@@ -110,9 +89,6 @@
}
.apps-view {
- width: 100%;
- height: 100%;
-
.mat-mdc-header-cell {
font-size: 15px;
font-weight: 500;
diff --git a/src/app/components/apps-view/apps-view.component.ts b/src/app/components/apps-view/apps-view.component.ts
index d742aac9..f24ed111 100644
--- a/src/app/components/apps-view/apps-view.component.ts
+++ b/src/app/components/apps-view/apps-view.component.ts
@@ -16,15 +16,14 @@
* limitations under the License.
*/
-import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
-import { MatSelectChange, MatSelect } from '@angular/material/select';
-import { finalize, debounceTime, distinctUntilChanged } from 'rxjs/operators';
+import { MatSelectChange } from '@angular/material/select';
+import { finalize } from 'rxjs/operators';
import { NgxSpinnerService } from 'ngx-spinner';
-import { fromEvent } from 'rxjs';
import { SchedulerService } from '@app/services/scheduler/scheduler.service';
import { AppInfo } from '@app/models/app-info.model';
@@ -32,9 +31,10 @@ import { AllocationInfo } from '@app/models/alloc-info.model';
import { ColumnDef } from '@app/models/column-def.model';
import { CommonUtil } from '@app/utils/common.util';
import { PartitionInfo } from '@app/models/partition-info.model';
-import { DropdownItem } from '@app/models/dropdown-item.model';
import { QueueInfo } from '@app/models/queue-info.model';
import { MatDrawer } from '@angular/material/sidenav';
+import { QueueNode } from '../queue-menu-tree/queue-menu-tree.component';
+import { FormControl } from '@angular/forms';
@Component({
selector: 'app-applications-view',
@@ -46,9 +46,8 @@ export class AppsViewComponent implements OnInit {
@ViewChild('allocationMatPaginator', { static: true }) allocPaginator!: MatPaginator;
@ViewChild('appSort', { static: true }) appSort!: MatSort;
@ViewChild('allocSort', { static: true }) allocSort!: MatSort;
- @ViewChild('searchInput', { static: true }) searchInput!: ElementRef;
- @ViewChild('queueSelect', { static: false }) queueSelect!: MatSelect;
@ViewChild('matDrawer', { static: false }) matDrawer!: MatDrawer;
+ searchControl = new FormControl('',{ nonNullable: true });
appDataSource = new MatTableDataSource([]);
appColumnDef: ColumnDef[] = [];
@@ -60,10 +59,9 @@ export class AppsViewComponent implements OnInit {
selectedRow: AppInfo | null = null;
initialAppData: AppInfo[] = [];
- searchText = '';
partitionList: PartitionInfo[] = [];
partitionSelected = '';
- leafQueueList: DropdownItem[] = [];
+ leafQueueList: QueueNode[] = [];
leafQueueSelected = '';
detailToggle: boolean = false;
@@ -125,12 +123,6 @@ export class AppsViewComponent implements OnInit {
this.allocColumnIds = this.allocColumnDef.map((col) => col.colId);
- fromEvent(this.searchInput.nativeElement, 'keyup')
- .pipe(debounceTime(500), distinctUntilChanged())
- .subscribe(() => {
- this.onSearchAppData();
- });
-
this.scheduler
.fetchPartitionList()
.pipe(
@@ -148,7 +140,7 @@ export class AppsViewComponent implements OnInit {
} else {
this.partitionList = [new PartitionInfo('-- Select --', '')];
this.partitionSelected = '';
- this.leafQueueList = [new DropdownItem('-- Select --', '')];
+ this.leafQueueList = [];
this.leafQueueSelected = '';
this.appDataSource.data = [];
this.clearQueueSelection();
@@ -169,44 +161,54 @@ export class AppsViewComponent implements OnInit {
.subscribe((data) => {
if (data && data.rootQueue) {
const leafQueueList = this.generateLeafQueueList(data.rootQueue);
- this.leafQueueList = [new DropdownItem('-- Select --', ''), ...leafQueueList];
+ this.leafQueueList = leafQueueList;
if (!this.fetchApplicationsUsingQueryParams()) this.setDefaultQueue(leafQueueList);
} else {
- this.leafQueueList = [new DropdownItem('-- Select --', '')];
+ this.leafQueueList = [];
}
});
}
- setDefaultQueue(queueList: DropdownItem[]): void {
+ setDefaultQueue(queueList: QueueNode[]): 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;
+ const storedQueueNode = this.findQueueNodeByValue(queueList, storedQueue);
+ if (storedQueueNode) {
+ this.leafQueueSelected = storedQueueNode.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));
+ findQueueNodeByValue(queueList: QueueNode[] = [], value:string): QueueNode | null {
+ for (const node of queueList) {
+ if (node.value === value) return node;
+
+ const result = this.findQueueNodeByValue(node.children, value);
+ if (result) return result;
}
+
+ return null;
+ }
- if (rootQueue && rootQueue.children) {
- rootQueue.children.forEach((child) => this.generateLeafQueueList(child, list));
+ generateLeafQueueList(rootQueue: QueueInfo, list: QueueNode[] = []): QueueNode[] {
+ if (rootQueue) {
+ list.push({
+ name: rootQueue.queueName.split(".").at(-1) || rootQueue.queueName,
+ value: rootQueue.queueName,
+ children: rootQueue.children
+ ? rootQueue.children.flatMap(node=> this.generateLeafQueueList(node, []))
+ : []
+ })
}
return list;
@@ -305,13 +307,13 @@ export class AppsViewComponent implements OnInit {
}
onClearSearch() {
- this.searchText = '';
+ this.searchControl.setValue('');
this.removeRowSelection();
this.appDataSource.data = this.initialAppData;
}
onSearchAppData() {
- const searchTerm = this.searchText.trim().toLowerCase();
+ const searchTerm = this.searchControl.value?.trim().toLowerCase();
if (searchTerm) {
this.removeRowSelection();
@@ -325,14 +327,14 @@ export class AppsViewComponent implements OnInit {
onPartitionSelectionChanged(selected: MatSelectChange) {
if (selected.value !== '') {
- this.searchText = '';
+ this.searchControl.setValue('');
this.partitionSelected = selected.value;
this.appDataSource.data = [];
this.removeRowSelection();
this.clearQueueSelection();
this.fetchQueuesForPartition(this.partitionSelected);
} else {
- this.searchText = '';
+ this.searchControl.setValue('');
this.partitionSelected = '';
this.leafQueueSelected = '';
this.appDataSource.data = [];
@@ -341,16 +343,16 @@ export class AppsViewComponent implements OnInit {
}
}
- onQueueSelectionChanged(selected: MatSelectChange) {
- if (selected.value !== '') {
- this.searchText = '';
- this.leafQueueSelected = selected.value;
+ onQueueSelectionChanged(selected: string) {
+ if (selected !== '') {
+ this.searchControl.setValue('');
+ this.leafQueueSelected = selected;
this.appDataSource.data = [];
this.removeRowSelection();
this.fetchAppListForPartitionAndQueue(this.partitionSelected, this.leafQueueSelected);
CommonUtil.setStoredQueueAndPartition(this.partitionSelected, this.leafQueueSelected);
} else {
- this.searchText = '';
+ this.searchControl.setValue('');
this.leafQueueSelected = '';
this.appDataSource.data = [];
this.removeRowSelection();
@@ -385,11 +387,6 @@ export class AppsViewComponent implements OnInit {
clearQueueSelection() {
CommonUtil.setStoredQueueAndPartition('');
this.leafQueueSelected = '';
- this.openQueueSelection();
- }
-
- openQueueSelection() {
- this.queueSelect.open();
}
toggle() {
@@ -412,4 +409,9 @@ export class AppsViewComponent implements OnInit {
.writeText(copyString)
.catch((error) => console.error('Writing to the clipboard is not allowed. ', error));
}
+
+ onChangeSearchText(newSearchText: string) {
+ this.searchControl.setValue(newSearchText);
+ this.onSearchAppData();
+ }
}
diff --git a/src/app/components/nodes-view/nodes-view.component.html b/src/app/components/nodes-view/nodes-view.component.html
index fb40cc1e..ce97274a 100644
--- a/src/app/components/nodes-view/nodes-view.component.html
+++ b/src/app/components/nodes-view/nodes-view.component.html
@@ -18,12 +18,9 @@
-
-
-
+
+ Partition
+
{{ part.name }}
@@ -32,109 +29,92 @@
-
-
-
-
-
-
+
+
-
+
-
-
-
- {{ columnDef.colName }}
+
+
+ {{ columnDef.colName }}
-
+ ">
+
+
+
+
+
+
+ - =2 && detailToggle"
+ [innerHTML]="resource | highlightSearch: searchControl.value">
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- - =2 && detailToggle"
- [innerHTML]="resource | highlightSearch: filterValue">
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+ No records found
+
+
-
-
- No records found
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
Allocations
@@ -149,18 +129,18 @@
Allocations
-
-
- -
- {{resource}}
-
- - =2 && detailToggle">
- {{resource}}
-
-
-
+
+
+ -
+ {{resource}}
+
+ - =2 && detailToggle">
+ {{resource}}
+
+
+
-
+
{{ element[columnDef.colId] }}
@@ -182,18 +162,12 @@ Allocations
-
+
-
+
\ No newline at end of file
diff --git a/src/app/components/nodes-view/nodes-view.component.scss b/src/app/components/nodes-view/nodes-view.component.scss
index aa2cd8b4..ae59145a 100644
--- a/src/app/components/nodes-view/nodes-view.component.scss
+++ b/src/app/components/nodes-view/nodes-view.component.scss
@@ -35,13 +35,8 @@
justify-content: flex-end;
align-items: center;
width: 35%;
- .filter{
- width: 100%;
- margin: 0 30px;
- .mat-mdc-form-field{
- width: 100%;
- }
- }
+ gap: 12px;
+
.btn-wrapper {
filter: drop-shadow(0px 2px 1px rgba(90, 90, 90, 0.5));
&:hover{
diff --git a/src/app/components/nodes-view/nodes-view.component.ts b/src/app/components/nodes-view/nodes-view.component.ts
index 6d199e20..da6cdefb 100644
--- a/src/app/components/nodes-view/nodes-view.component.ts
+++ b/src/app/components/nodes-view/nodes-view.component.ts
@@ -30,6 +30,7 @@ import { AllocationInfo } from '@app/models/alloc-info.model';
import { ColumnDef } from '@app/models/column-def.model';
import { CommonUtil } from '@app/utils/common.util';
import { PartitionInfo } from '@app/models/partition-info.model';
+import { FormControl } from '@angular/forms';
@Component({
selector: 'app-nodes-view',
@@ -41,7 +42,7 @@ export class NodesViewComponent implements OnInit {
@ViewChild('allocationMatPaginator', { static: true }) allocPaginator!: MatPaginator;
@ViewChild('nodeSort', { static: true }) nodeSort!: MatSort;
@ViewChild('allocSort', { static: true }) allocSort!: MatSort;
-
+
nodeDataSource = new MatTableDataSource([]);
nodeColumnDef: ColumnDef[] = [];
nodeColumnIds: string[] = [];
@@ -55,7 +56,7 @@ export class NodesViewComponent implements OnInit {
partitionSelected = '';
detailToggle: boolean = false;
- filterValue: string = '';
+ searchControl = new FormControl('',{ nonNullable: true });
constructor(
private scheduler: SchedulerService,
@@ -296,9 +297,9 @@ export class NodesViewComponent implements OnInit {
return objectString.includes(filter);
};
- applyFilter(event: Event): void {
- this.filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
- this.nodeDataSource.filter = this.filterValue;
+ onChangeSearchText(newSearchText: string) {
+ this.searchControl.setValue(newSearchText.trim().toLowerCase());
+ this.nodeDataSource.filter = this.searchControl.value;
this.nodeDataSource.filterPredicate = this.filterPredicate;
}
}
diff --git a/src/app/components/queue-menu-tree/queue-menu-tree.component.html b/src/app/components/queue-menu-tree/queue-menu-tree.component.html
new file mode 100644
index 00000000..b577b763
--- /dev/null
+++ b/src/app/components/queue-menu-tree/queue-menu-tree.component.html
@@ -0,0 +1,57 @@
+
+
+
\ No newline at end of file
diff --git a/src/app/components/queue-menu-tree/queue-menu-tree.component.scss b/src/app/components/queue-menu-tree/queue-menu-tree.component.scss
new file mode 100644
index 00000000..f96a6b75
--- /dev/null
+++ b/src/app/components/queue-menu-tree/queue-menu-tree.component.scss
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ @use '@angular/material' as mat;
+
+ .tree-menu-wrapper {
+ position: relative;
+ padding: 8px;
+
+ .mat-mdc-icon-button.mat-mdc-button-base {
+ --mdc-icon-button-state-layer-size: 24px;
+ padding: 0px;
+ }
+
+ .header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 2px;
+ margin-bottom: 10px;
+
+ .actions {
+ display: flex;
+ justify-content: end;
+ align-items: center;
+
+ .mat-mdc-button {
+ color: #666;
+ font-size: 12px;
+ font-weight: 400;
+ padding: 2px;
+ height: auto;
+ min-width: unset;
+ }
+ }
+ }
+ }
+
+ mat-tree {
+ background-color: transparent;
+ }
+
+ mat-tree-node {
+ min-height: 32px;
+ }
+
+ .tree-node-label {
+ flex: 1;
+ padding: 4px 8px;
+ border-radius: 6px;
+ transition: .3s;
+ cursor: pointer;
+
+ &:not(.tree-node-active):hover {
+ background-color: #ddd;
+ }
+ }
+
+ .tree-node-active {
+ background-color: #7aacff6e;
+ }
+
+
+ .notched-outline {
+ display: flex;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100%;
+ height: 100%;
+ text-align: left;
+ pointer-events: none;
+
+ .notch-piece {
+ border-color: rgba(0, 0, 0, 0.38);
+ border-width: 1px;
+ box-sizing: border-box;
+ height: 100%;
+ pointer-events: none;
+ border-top: 1px solid;
+ border-bottom: 1px solid;
+ }
+
+ .notched-outline__leading {
+ width: 12px;
+ border-left: 1px solid;
+ border-right: none;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+
+ .notched-outline__notch {
+ border-left: none;
+ border-right: none;
+ border-top: none;
+ border-bottom: 1px solid;
+ padding: 0px 4px;
+
+ .floating-label {
+ position: relative;
+ font-size: 12px;
+ display: block;
+ transform: translateY(-50%);
+ }
+ }
+
+ .notched-outline__trailing {
+ flex-grow: 1;
+ border-right: 1px solid;
+ border-left: none;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+ }
\ No newline at end of file
diff --git a/src/app/components/queue-menu-tree/queue-menu-tree.component.ts b/src/app/components/queue-menu-tree/queue-menu-tree.component.ts
new file mode 100644
index 00000000..0a2220f3
--- /dev/null
+++ b/src/app/components/queue-menu-tree/queue-menu-tree.component.ts
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { CdkTree, NestedTreeControl } from '@angular/cdk/tree';
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
+import { MatTree } from '@angular/material/tree';
+
+export interface QueueNode {
+ name: string;
+ value: string;
+ children?: QueueNode[];
+}
+
+@Component({
+ selector: 'app-queue-menu-tree',
+ templateUrl: './queue-menu-tree.component.html',
+ styleUrls: ['./queue-menu-tree.component.scss'],
+})
+export class QueueMenuTreeComponent implements OnInit, OnChanges {
+ @Input() dataSource: QueueNode[] = [];
+ @Input() selectedNode = '';
+ @Output() selectedNodeChange = new EventEmitter();
+ @ViewChild('tree') tree: MatTree | undefined;
+
+ childrenAccessor = (node: QueueNode) => node.children ?? [];
+
+ hasChild = (_: number, node: QueueNode) => !!node.children && node.children.length > 0;
+
+ constructor() {}
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['selectedNode'] && !changes['selectedNode'].firstChange) {
+ this.expandSelectedNodeParents();
+ }
+ }
+
+ ngOnInit() {}
+
+ onSelectedNode (value: string){
+ if(this.selectedNode === value) this.selectedNode = '';
+ else this.selectedNode = value;
+ this.selectedNodeChange.emit(this.selectedNode);
+ }
+
+ expandAll () {
+ this.dataSource.forEach(node => this.tree?.expandDescendants(node))
+ }
+
+ collapseAll () {
+ this.dataSource.forEach(node => this.tree?.collapseDescendants(node));
+ this.expandSelectedNodeParents();
+ }
+
+ expandSelectedNodeParents() {
+ const parentNodes = this.findAllParentNodes(0, [], this.dataSource);
+ parentNodes.forEach(node => this.tree?.expand(node));
+ }
+
+ findAllParentNodes(index: number, parents: QueueNode[] = [], children: QueueNode[] = []) {
+ let nodes = this.selectedNode.split(".");
+ if(nodes.length-1 === index) return parents;
+
+ children.forEach(node=> {
+ if(node.name === nodes[index]) {
+ parents.push(node);
+ parents = this.findAllParentNodes(index+1, parents, node.children);
+ return;
+ }
+ });
+ return parents;
+ }
+}
diff --git a/src/app/components/search-input/search-input.component.html b/src/app/components/search-input/search-input.component.html
new file mode 100644
index 00000000..1195919e
--- /dev/null
+++ b/src/app/components/search-input/search-input.component.html
@@ -0,0 +1,36 @@
+
+
+
+ Search
+
+
+
+
\ No newline at end of file
diff --git a/src/app/components/search-input/search-input.component.scss b/src/app/components/search-input/search-input.component.scss
new file mode 100644
index 00000000..990d22e4
--- /dev/null
+++ b/src/app/components/search-input/search-input.component.scss
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ .search-wrapper {
+ width: 100%;
+ right: 20px;
+
+ input {
+ width: calc(100% - 22px);
+ color: #333;
+ }
+
+ .clear-btn {
+ outline: none;
+ border: none;
+ padding: 0 0 0 4px;
+ cursor: pointer;
+ background: transparent;
+
+ i {
+ font-size: 18px;
+
+ &:hover {
+ color: #f44336;
+ }
+ }
+ }
+
+ .search-icon {
+ margin-left: 4px;
+ font-size: 17px;
+ }
+}
\ No newline at end of file
diff --git a/src/app/components/search-input/search-input.component.ts b/src/app/components/search-input/search-input.component.ts
new file mode 100644
index 00000000..5870631d
--- /dev/null
+++ b/src/app/components/search-input/search-input.component.ts
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { debounceTime, distinctUntilChanged } from 'rxjs';
+
+@Component({
+ selector: 'app-search-input',
+ templateUrl: './search-input.component.html',
+ styleUrls: ['./search-input.component.scss'],
+})
+export class SearchInputComponent implements OnInit {
+ @Input() control = new FormControl('',{ nonNullable: true });
+ @Output() valueChange = new EventEmitter();
+
+ constructor() {}
+
+ ngOnInit() {
+ this.control.valueChanges
+ .pipe(
+ debounceTime(500),
+ distinctUntilChanged()
+ )
+ .subscribe(value => {
+ this.valueChange.emit(value);
+ });
+ }
+
+ onClearSearch() {
+ this.control.setValue('');
+ this.valueChange.emit('');
+ }
+}
diff --git a/src/styles.scss b/src/styles.scss
index 8dfc8b18..a6e44c76 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -18,6 +18,9 @@
@use '@angular/material' as mat;
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
+@import '~material-design-icons/iconfont/material-icons.css';
+
+$borderRadius: 8px;
* {
box-sizing: border-box;
@@ -139,3 +142,37 @@ p {
background-color: #fff;
padding: 0 5px 0 5px;
}
+
+.mat-mdc-table {
+ border-radius: $borderRadius;
+
+ .mat-mdc-header-row {
+ border-top-left-radius: $borderRadius;
+ border-top-right-radius: $borderRadius;
+
+ .mat-mdc-header-cell {
+ &:first-child {
+ border-top-left-radius: $borderRadius;
+ }
+
+ &:last-child {
+ border-top-right-radius: $borderRadius;
+ }
+ }
+ }
+
+ .mat-mdc-footer-row {
+ border-bottom-left-radius: $borderRadius;
+ border-bottom-right-radius: $borderRadius;
+
+ .mat-mdc-footer-cell {
+ border-bottom-left-radius: $borderRadius;
+ border-bottom-right-radius: $borderRadius;
+ }
+ }
+
+ & + .mat-mdc-paginator {
+ border-bottom-left-radius: $borderRadius;
+ border-bottom-right-radius: $borderRadius;
+ }
+}
\ No newline at end of file