From b743337cd6aec4fa0a6a54bced53de1b94071e7b Mon Sep 17 00:00:00 2001 From: SP12893678 <36910625+SP12893678@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:57:24 +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 | 243 ++++++------- .../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, 812 insertions(+), 405 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. --> -
-
- -
- - - - - -
- -
-
-
-
-
- - - {{ columnDef.colName }} - - - {{ element['formattedSubmissionTime'] }} - + + + + +
+
+ +
+
+
+ +
+
+
+ + + {{ columnDef.colName }} - - {{ element['formattedlastStateChangeTime'] }} - + + {{ element['formattedSubmissionTime'] }} + - - - - - -
    - -
  • - {{ resource }} -
  • -
  • - {{ resource }} -
  • -
    -
-
-
- - {{ element[columnDef.colId] }} - -
+ + {{ element['formattedlastStateChangeTime'] }} - + - + + +
    + +
  • + {{ resource }} +
  • +
  • + {{ resource }} +
  • +
    +
+
+
+ + {{ element[columnDef.colId] }} +
-
-
- - - {{ element[columnDef.colId] || 'n/a' }} - -
- - - - - - - - - - - - - -
No records found
-
-
- - - - - - -
- - -
- - - - -
- {{ selectedRow?.applicationId }} ({{ selectedRow?.allocations?.length }} allocations) - - -
-
- - - - {{ columnDef.colName }} - - - {{ element['priority'] }} + + + + + + + + - - - - -
    - -
  • - {{ resource }} -
  • -
  • - {{ resource }} -
  • + + {{ element[columnDef.colId] || 'n/a' }} + +
    + + + + + + + + + + + + + +
    No records found
    +
    +
    + + + + + + + + + + + + + +
    + {{ selectedRow?.applicationId }} ({{ selectedRow?.allocations?.length }} allocations) + + +
    +
    + + + + {{ + columnDef.colName }} + + + {{ + element['priority'] }} + + + + + + +
      + +
    • + {{ resource }} +
    • +
    • + {{ 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..2e9b1a6a 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 @@
-
- - - -
- + +
- +
-
- - - {{ columnDef.colName }} + + + {{ columnDef.colName }} - + "> + + + +
    + +
  • +
  • +
    +
+
+
+ + + +
+
+ + + + - - -
    - -
  • -
  • -
    -
+ +
+
+ + + +
    + +
  • - - - - +
+
+
- - - - - - - - - - -
    - -
  • -
    -
-
-
-
-
+ + + + + + + + + - - - - - - - - - + + +
No records found
+
+
- - -
No records found
-
-
+ - + - + +
- - + - -

Allocations

-
- - - {{ columnDef.colName }} - - - - - -
    - -
  • - {{resource}} -
  • -
  • - {{resource}} -
  • -
    -
-
-
- - {{ element[columnDef.colId] }} - -
-
+ + + {{ columnDef.colName }} - - {{ element[columnDef.colId] || 'n/a' }} - + + + + +
    + +
  • + {{resource}} +
  • +
  • + {{resource}} +
  • +
    +
+
+
+ + {{ element[columnDef.colId] }} + +
- - -
No records found
-
-
+ + {{ element[columnDef.colId] || 'n/a' }} + +
+ + + +
No records found
+
+
- + - + - -
+ +
+ + - -
\ 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 @@ + + +
+
+
+
+ +
/
+ +
+
+ + + + +
+ {{node.name}} +
+
+ + + +
+ {{node.name}} +
+
+
+
+
+
+ +
+
+
+
\ 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