This is an automated email from the ASF dual-hosted git repository.
wilfreds 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 1a51966 [YUNIKORN-1362] Filtering node list on UI (#153)
1a51966 is described below
commit 1a51966f637852b8e1ef53e85744cdf88883cb4b
Author: wusamzong <[email protected]>
AuthorDate: Thu Jan 25 14:16:55 2024 +1100
[YUNIKORN-1362] Filtering node list on UI (#153)
Filter nodes based on the node attributes:
* Attributes are exposed in the node table (detail mode)
* A filter input has been introduced
* Nodes not matching filter are hidden
* The filter results are highlighted
Two nodes have been explicitly added to db.json for testing and validating
the filter functionality.
Closes: #153
Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
json-db.json | 130 +++++++++++++++++++++
src/app/app.module.ts | 2 +
.../nodes-view/highlighttable-search.pipe.ts} | 31 +++--
.../nodes-view/nodes-view.component.html | 48 ++++++--
.../nodes-view/nodes-view.component.scss | 56 ++++++---
.../nodes-view/nodes-view.component.spec.ts | 8 ++
.../components/nodes-view/nodes-view.component.ts | 71 ++++++++++-
src/app/models/node-info.model.ts | 7 +-
src/app/services/scheduler/scheduler.service.ts | 3 +-
src/styles.scss | 5 -
10 files changed, 306 insertions(+), 55 deletions(-)
diff --git a/json-db.json b/json-db.json
index d495481..4dede38 100644
--- a/json-db.json
+++ b/json-db.json
@@ -589,6 +589,20 @@
"nodes": [
{
"nodeID": "lima-rancher-desktop",
+ "attributes":{
+ "beta.kubernetes.io/arch":"amd64",
+ "beta.kubernetes.io/os":"linux",
+ "kubernetes.io/arch":"amd64",
+ "kubernetes.io/hostname":"lima-rancher-desktop",
+ "kubernetes.io/os":"linux",
+ "node-role.kubernetes.io/control-plane":"",
+ "node.kubernetes.io/exclude-from-external-load-balancers":"",
+ "ready":"true",
+ "si.io/hostname":"lima-rancher-desktop",
+ "si.io/rackname":"/rack-default",
+ "si/instance-type":"",
+ "si/node-partition":"[mycluster]default"
+ },
"hostName": "",
"rackName": "",
"capacity": {
@@ -749,6 +763,122 @@
"schedulable": true,
"isReserved": false,
"reservations": []
+ },
+ {
+ "nodeID": "lima-rancher-desktop2",
+ "attributes":{
+ "beta.kubernetes.io/arch":"arm64",
+ "beta.kubernetes.io/os":"linux",
+ "kubernetes.io/arch":"arm64",
+ "kubernetes.io/hostname":"lima-rancher-desktop2",
+ "kubernetes.io/os":"linux",
+ "node-role.kubernetes.io/control-plane":"",
+ "node.kubernetes.io/exclude-from-external-load-balancers":"",
+ "ready":"true",
+ "si.io/hostname":"lima-rancher-desktop2",
+ "si.io/rackname":"/rack-default",
+ "si/instance-type":"",
+ "si/node-partition":"[mycluster]default"
+ },
+ "hostName": "",
+ "rackName": "",
+ "capacity": {
+ "ephemeral-storage": 99833802265,
+ "hugepages-1Gi": 0,
+ "hugepages-2Mi": 0,
+ "hugepages-32Mi": 0,
+ "hugepages-64Ki": 0,
+ "memory": 4110970880,
+ "pods": 110,
+ "vcore": 2000
+ },
+ "allocated": {
+ "memory": 0,
+ "pods": 0,
+ "vcore": 0
+ },
+ "occupied": {
+ "memory": 0,
+ "pods": 0,
+ "vcore": 0
+ },
+ "available": {
+ "ephemeral-storage": 99833802265,
+ "hugepages-1Gi": 0,
+ "hugepages-2Mi": 0,
+ "hugepages-32Mi": 0,
+ "hugepages-64Ki": 0,
+ "memory": 4110970880,
+ "pods": 110,
+ "vcore": 2000
+ },
+ "utilized": {
+ "memory": 0,
+ "pods": 0,
+ "vcore": 0
+ },
+ "allocations": [],
+ "schedulable": true,
+ "isReserved": false,
+ "reservations": []
+ },
+ {
+ "nodeID": "lima-rancher-desktop3",
+ "attributes":{
+ "beta.kubernetes.io/arch":"arm64",
+ "beta.kubernetes.io/os":"linux",
+ "kubernetes.io/arch":"arm64",
+ "kubernetes.io/hostname":"lima-rancher-desktop3",
+ "kubernetes.io/os":"linux",
+ "node-role.kubernetes.io/control-plane":"",
+ "node.kubernetes.io/exclude-from-external-load-balancers":"",
+ "ready":"true",
+ "si.io/hostname":"lima-rancher-desktop3",
+ "si.io/rackname":"/rack-default",
+ "si/instance-type":"",
+ "si/node-partition":"[mycluster]default"
+ },
+ "hostName": "",
+ "rackName": "",
+ "capacity": {
+ "ephemeral-storage": 99833802265,
+ "hugepages-1Gi": 0,
+ "hugepages-2Mi": 0,
+ "hugepages-32Mi": 0,
+ "hugepages-64Ki": 0,
+ "memory": 4110970880,
+ "pods": 110,
+ "vcore": 2000
+ },
+ "allocated": {
+ "memory": 0,
+ "pods": 0,
+ "vcore": 0
+ },
+ "occupied": {
+ "memory": 0,
+ "pods": 0,
+ "vcore": 0
+ },
+ "available": {
+ "ephemeral-storage": 99833802265,
+ "hugepages-1Gi": 0,
+ "hugepages-2Mi": 0,
+ "hugepages-32Mi": 0,
+ "hugepages-64Ki": 0,
+ "memory": 4110970880,
+ "pods": 110,
+ "vcore": 2000
+ },
+ "utilized": {
+ "memory": 0,
+ "pods": 0,
+ "vcore": 0
+ },
+ "allocations": [],
+ "schedulable": true,
+ "isReserved": false,
+ "reservations": []
}
],
"node-utilization": {
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 6cc8fc8..a325942 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -55,6 +55,7 @@ import { ContainerHistoryComponent } from
'@app/components/container-history/con
import { QueueRackComponent } from
'@app/components/queue-rack/queue-rack.component';
import { AppsViewComponent } from
'@app/components/apps-view/apps-view.component';
import { NodesViewComponent } from
'@app/components/nodes-view/nodes-view.component';
+import { HighlightSearchPipe } from
'@app/components/nodes-view/highlighttable-search.pipe';
import { ErrorViewComponent } from
'@app/components/error-view/error-view.component';
import { StatusViewComponent } from
'@app/components/status-view/status-view.component';
import { HealthchecksComponent } from
'@app/components/healthchecks/healthchecks.component';
@@ -75,6 +76,7 @@ import { BarChartComponent } from
'@app/components/bar-chart/bar-chart.component
QueueRackComponent,
AppsViewComponent,
NodesViewComponent,
+ HighlightSearchPipe,
ErrorViewComponent,
StatusViewComponent,
HealthchecksComponent,
diff --git a/src/app/models/node-info.model.ts
b/src/app/components/nodes-view/highlighttable-search.pipe.ts
similarity index 50%
copy from src/app/models/node-info.model.ts
copy to src/app/components/nodes-view/highlighttable-search.pipe.ts
index 41b1bb6..3f10da9 100644
--- a/src/app/models/node-info.model.ts
+++ b/src/app/components/nodes-view/highlighttable-search.pipe.ts
@@ -16,24 +16,21 @@
* limitations under the License.
*/
-import { AllocationInfo } from './alloc-info.model';
+import { Pipe, PipeTransform } from '@angular/core';
-export class NodeInfo {
- isSelected = false;
- constructor(
- public nodeId: string,
- public hostName: string,
- public rackName: string,
- public partitionName: string,
- public capacity: string,
- public allocated: string,
- public occupied: string,
- public available: string,
- public utilized: string,
- public allocations: AllocationInfo[] | null
- ) {}
+@Pipe({
+ name: 'highlightSearch'
+})
+export class HighlightSearchPipe implements PipeTransform {
- setAllocations(allocs: AllocationInfo[]) {
- this.allocations = allocs;
+ transform(value: string, search: string): string {
+ const valueStr = String(value); // Ensure numeric values are converted to
strings
+ // (?![^&;]+;) - to ensure that there are no HTML entities (such as &
or <)
+ // ex. value = "<span>" search = "span", The word
'span' will not be included in the matches
+ // (?!<[^<>]*) - to ensure that there are no HTML tags (<...>)
+ // ex. value = "<span>" search = "span", The word 'span'
will not be included in the matches
+ return valueStr.replace(new RegExp('(?![^&;]+;)(?!<[^<>]*)(' + search +
')(?![^<>]*>)(?![^&;]+;)', 'gi'), '<strong>$1</strong>');
}
+
}
+
diff --git a/src/app/components/nodes-view/nodes-view.component.html
b/src/app/components/nodes-view/nodes-view.component.html
index 2b9ea57..17b8f8b 100644
--- a/src/app/components/nodes-view/nodes-view.component.html
+++ b/src/app/components/nodes-view/nodes-view.component.html
@@ -30,11 +30,22 @@
</mat-select>
</mat-form-field>
</div>
- <div class="btn-wrapper">
- <button class="btn" (click)="toggle()"
[style.background-color]="detailToggle ? '#313d54' : '#f5f5f5'">
- <i class="material-icons __icon" [style.color]="detailToggle ? '#f5f5f5'
: '#313d54' ">more_horiz</i>
- </button>
+
+ <div class="right-wrapper">
+ <div class="filter">
+ <mat-form-field>
+ <mat-label>Filter</mat-label>
+ <input matInput (keyup)="applyFilter($event)" placeholder="Ex. amd64"
#input>
+ </mat-form-field>
+ </div>
+
+ <div class="btn-wrapper">
+ <button class="btn" (click)="toggle()"
[style.background-color]="detailToggle ? '#313d54' : '#f5f5f5'">
+ <i class="material-icons __icon" [style.color]="detailToggle ?
'#f5f5f5' : '#313d54' ">more_horiz</i>
+ </button>
+ </div>
</div>
+
</div>
<div class="nodes-view">
<div class="mat-elevation-z8">
@@ -53,24 +64,37 @@
<ng-container
*ngIf="columnDef.colFormatter(element[columnDef.colId]) as colValue">
<ul class="mat-res-ul">
<ng-container *ngFor="let resource of
formatResources(colValue); let i = index">
- <li class="mat-res-li" *ngIf="i<2">
- {{resource}}
- </li>
- <li class="mat-res-li" *ngIf="i>=2 && detailToggle">
- {{resource}}
- </li>
+ <li class="mat-res-li" *ngIf="i<2"
+ [innerHTML]="resource | highlightSearch:
filterValue"></li>
+ <li class="mat-res-li" *ngIf="i>=2 && detailToggle"
+ [innerHTML]="resource | highlightSearch:
filterValue"></li>
</ng-container>
</ul>
</ng-container>
</ng-container>
<ng-template #showNodeRawData>
- <span>{{ element[columnDef.colId] }}</span>
+ <span [innerHTML]="element[columnDef.colId] | highlightSearch:
filterValue"></span>
</ng-template>
</mat-cell>
</ng-container>
<ng-template #renderNext_1>
- <mat-cell *matCellDef="let element">{{ element[columnDef.colId] ||
'n/a' }}</mat-cell>
+
+ <ng-container *ngIf="columnDef.colId!=='attributes'">
+ <mat-cell *matCellDef="let element">
+ <span [innerHTML]="element[columnDef.colId] || 'n/a' |
highlightSearch: filterValue"></span>
+ </mat-cell>
+ </ng-container>
+
+ <ng-container *ngIf="columnDef.colId=='attributes'">
+ <mat-cell *matCellDef="let element" class="mat-attr">
+ <ul class="mat-attr-ul">
+ <ng-container *ngFor="let attribute of
formatAttribute(element[columnDef.colId]); let i = index">
+ <li class="mat-attr-li" [innerHTML]="attribute |
highlightSearch: filterValue"></li>
+ </ng-container>
+ </ul>
+ </mat-cell>
+ </ng-container>
</ng-template>
</ng-container>
diff --git a/src/app/components/nodes-view/nodes-view.component.scss
b/src/app/components/nodes-view/nodes-view.component.scss
index 545d27b..2dd019f 100644
--- a/src/app/components/nodes-view/nodes-view.component.scss
+++ b/src/app/components/nodes-view/nodes-view.component.scss
@@ -29,26 +29,41 @@
margin-right: 10px;
}
}
- .btn-wrapper {
- filter: drop-shadow(0px 2px 1px rgba(90, 90, 90, 0.5));
- &:hover{
- filter: drop-shadow(0px 3px 3px rgba(90, 90, 90, 0.5));
- }
- :hover{
- cursor: pointer;
- }
- .btn{
- display: block;
- border: none;
- padding: 13px 24px;
- border-radius: 5px;
- font-size: 24px;
- transform: translateY(-13px);
+ .right-wrapper{
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ width: 35%;
+ .filter{
+ width: 100%;
+ margin: 0 30px;
+ .mat-mdc-form-field{
+ width: 100%;
+ }
}
- .material-icons{
- transform: translateY(2px);
+ .btn-wrapper {
+ filter: drop-shadow(0px 2px 1px rgba(90, 90, 90, 0.5));
+ &:hover{
+ filter: drop-shadow(0px 3px 3px rgba(90, 90, 90, 0.5));
+ }
+ :hover{
+ cursor: pointer;
+ }
+ .btn{
+ display: block;
+ border: none;
+ padding: 13px 24px;
+ border-radius: 5px;
+ font-size: 24px;
+ transform: translateY(-13px);
+ }
+ .material-icons{
+ transform: translateY(2px);
+ }
}
}
+
}
.nodes-view {
@@ -79,6 +94,13 @@
color: #8d00d4;
}
}
+ .mat-attr-ul{
+ padding: 0;
+ margin: 0;
+ .mat-attr-li{
+ list-style-type: none;
+ }
+ }
}
.mat-mdc-row {
&:hover {
diff --git a/src/app/components/nodes-view/nodes-view.component.spec.ts
b/src/app/components/nodes-view/nodes-view.component.spec.ts
index 9f61f99..17ca95c 100644
--- a/src/app/components/nodes-view/nodes-view.component.spec.ts
+++ b/src/app/components/nodes-view/nodes-view.component.spec.ts
@@ -29,6 +29,9 @@ import { MatPaginatorModule } from
'@angular/material/paginator';
import { MatDividerModule } from '@angular/material/divider';
import { MatSortModule } from '@angular/material/sort';
import { MatSelectModule } from '@angular/material/select';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { ReactiveFormsModule } from '@angular/forms';
describe('NodesViewComponent', () => {
let component: NodesViewComponent;
@@ -54,6 +57,11 @@ describe('NodesViewComponent', () => {
});
beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [NodesViewComponent],
+ imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule],
+ })
+ .compileComponents();
fixture = TestBed.createComponent(NodesViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
diff --git a/src/app/components/nodes-view/nodes-view.component.ts
b/src/app/components/nodes-view/nodes-view.component.ts
index 081dbb2..a9c77d7 100644
--- a/src/app/components/nodes-view/nodes-view.component.ts
+++ b/src/app/components/nodes-view/nodes-view.component.ts
@@ -22,7 +22,7 @@ import { MatSelectChange } from '@angular/material/select';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { NgxSpinnerService } from 'ngx-spinner';
-import { finalize, flatMap } from 'rxjs/operators';
+import { finalize } from 'rxjs/operators';
import { SchedulerService } from '@app/services/scheduler/scheduler.service';
import { NodeInfo } from '@app/models/node-info.model';
@@ -56,6 +56,7 @@ export class NodesViewComponent implements OnInit {
partitionSelected = '';
detailToggle: boolean = false;
+ filterValue: string = '';
constructor(private scheduler: SchedulerService, private spinner:
NgxSpinnerService) {}
@@ -69,6 +70,7 @@ export class NodesViewComponent implements OnInit {
{ colId: 'nodeId', colName: 'Node ID' },
{ colId: 'rackName', colName: 'Rack Name' },
{ colId: 'hostName', colName: 'Host Name' },
+ { colId: 'attributes', colName: 'Attributes' },
{ colId: 'capacity', colName: 'Capacity', colFormatter:
CommonUtil.resourceColumnFormatter },
{ colId: 'occupied', colName: 'Used', colFormatter:
CommonUtil.resourceColumnFormatter },
{
@@ -139,6 +141,7 @@ export class NodesViewComponent implements OnInit {
)
.subscribe((data) => {
this.nodeDataSource.data = data;
+ this.formatColumn();
});
}
@@ -189,6 +192,28 @@ export class NodesViewComponent implements OnInit {
return this.allocDataSource.data && this.allocDataSource.data.length === 0;
}
+ formatColumn(){
+ if(this.nodeDataSource.data.length==0){
+ return
+ }
+ this.nodeColumnIds.forEach((colId)=>{
+ let emptyCell=this.nodeDataSource.data.filter((node: NodeInfo)=>{
+ if (colId === 'indicatorIcon'){
+ return false;
+ }
+ if (!(colId in node)) {
+ console.error(`Property '${colId}' does not exist on Node.`);
+ return false;
+ }
+ return (node as any)[colId]==="" || (node as any)[colId]==="n/a";
+ })
+ if (emptyCell.length==this.nodeDataSource.data.length){
+ 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();
@@ -196,7 +221,7 @@ export class NodesViewComponent implements OnInit {
}
formatResources(colValue:string):string[]{
- const arr:string[]=colValue.split("<br/>")
+ 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"));
@@ -215,7 +240,49 @@ export class NodesViewComponent implements OnInit {
return result;
}
+ formatAttribute(attributes:any):string[]{
+ let result:string[]=[];
+ Object.entries(attributes).forEach(entry=>{
+ const [key, value] = entry;
+ if (value==="" || key.includes("si")){
+ return
+ }
+ result.push(key+':'+value);
+ })
+ return result;
+ }
+
toggle(){
this.detailToggle = !this.detailToggle;
+ this.displayAttribute(this.detailToggle);
+ }
+
+ 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");
+ }
+ }
+
+ 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=>{
+ const [key, value] = entry;
+ deepCopy.attributes[key]= `${key}:${value}`
+ })
+ const objectString = JSON.stringify(deepCopy).toLowerCase();
+ return objectString.includes(filter);
+ };
+
+ applyFilter(event: Event): void {
+ this.filterValue = (event.target as
HTMLInputElement).value.trim().toLowerCase();
+ this.nodeDataSource.filter = this.filterValue;
+ this.nodeDataSource.filterPredicate = this.filterPredicate;
}
}
diff --git a/src/app/models/node-info.model.ts
b/src/app/models/node-info.model.ts
index 41b1bb6..3c5858a 100644
--- a/src/app/models/node-info.model.ts
+++ b/src/app/models/node-info.model.ts
@@ -30,10 +30,15 @@ export class NodeInfo {
public occupied: string,
public available: string,
public utilized: string,
- public allocations: AllocationInfo[] | null
+ public allocations: AllocationInfo[] | null,
+ public attributes: Attributes,
) {}
setAllocations(allocs: AllocationInfo[]) {
this.allocations = allocs;
}
}
+
+export interface Attributes{
+ [key: string]: string;
+}
diff --git a/src/app/services/scheduler/scheduler.service.ts
b/src/app/services/scheduler/scheduler.service.ts
index 993c2c1..012f307 100644
--- a/src/app/services/scheduler/scheduler.service.ts
+++ b/src/app/services/scheduler/scheduler.service.ts
@@ -205,7 +205,8 @@ export class SchedulerService {
this.formatResource(node['occupied'] as SchedulerResourceInfo),
this.formatResource(node['available'] as SchedulerResourceInfo),
this.formatPercent(node['utilized'] as SchedulerResourceInfo),
- []
+ [],
+ node['attributes'],
);
const allocations = node['allocations'];
diff --git a/src/styles.scss b/src/styles.scss
index ec0c605..650a4bb 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -95,11 +95,6 @@ body {
color: #666;
}
-strong {
- font-weight: 500;
- color: #333;
-}
-
p {
margin: 10px 0;
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]