diff --git a/web/mock-server/server.json b/web/mock-server/server.json index 2829c2f6..50ec965c 100644 --- a/web/mock-server/server.json +++ b/web/mock-server/server.json @@ -1,4 +1,4 @@ { - "port": 3000, + "port": 9889, "routes": "mock-server/routes.json" } diff --git a/web/package-lock.json b/web/package-lock.json index d13c8cb3..af322868 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8362,6 +8362,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9462,6 +9468,15 @@ "node": ">= 4" } }, + "node_modules/lodash-id": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz", + "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9875,6 +9890,36 @@ "dev": true, "license": "MIT" }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "dev": true, + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -11471,6 +11516,24 @@ "node": ">=4" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", @@ -13014,6 +13077,15 @@ "graceful-fs": "^4.1.3" } }, + "node_modules/steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.3" + } + }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", diff --git a/web/src/app/allocations-drawer/allocations-drawer.component.html b/web/src/app/allocations-drawer/allocations-drawer.component.html index a58eb50a..734cf3a0 100644 --- a/web/src/app/allocations-drawer/allocations-drawer.component.html +++ b/web/src/app/allocations-drawer/allocations-drawer.component.html @@ -1,82 +1,123 @@ - +
- {{ selectedRow?.applicationId }} ({{ selectedRow?.allocations?.length }} allocations) - + {{ selectedRow?.applicationId }} ({{ allocDataSource?.data?.length }} instances) + +
+
+ + + Show all instances + {{ instance }} + + + + + + {{ state }} + + + + + + {{ node }} + + +
- + - {{ - columnDef.colName }} - - - {{ - element['priority'] }} - - - - Logs - + + {{ columnDef.colName }} + - - - - -
    - -
  • - {{ resource }} -
  • -
  • - {{ resource }} -
  • + + + + + + + +
      + +
    • {{ resource }}
    • +
      +
    -
-
+
+ + {{ element[columnDef.colId] }} + +
- - {{ element[columnDef.colId] }} - - -
- - - {{ element[columnDef.colId] || 'n/a' - }} - + + + + {{ element[columnDef.colId] || 'n/a' }} + + + + +
No records found
+ + + +
+ + + +
+
+ - - - - + +
+ + {{ selectedAllocation?.displayName }} +
+ + + + + {{element.key}} + + + + + + + {{element.value}} + + + {{element.value}} + + + + + + + + +
+ +
-
\ No newline at end of file + diff --git a/web/src/app/allocations-drawer/allocations-drawer.component.scss b/web/src/app/allocations-drawer/allocations-drawer.component.scss index e60de1ba..ae0af4b8 100644 --- a/web/src/app/allocations-drawer/allocations-drawer.component.scss +++ b/web/src/app/allocations-drawer/allocations-drawer.component.scss @@ -1,3 +1,5 @@ +@import 'https://fonts.googleapis.com/icon?family=Material+Icons'; +@import "@angular/material/prebuilt-themes/deeppurple-amber.css"; .mat-drawer-container { min-width: 430px; width: 55%; @@ -83,6 +85,11 @@ color: #f44336; } } + + .arrow-btn { + font-size: 1.2em; + padding-right: 10px; + } .copy-btn { font-size: 1em; cursor: pointer; @@ -135,6 +142,27 @@ } .row { - min-height: unset; + min-height: 30px; + } + .mat-mdc-row:nth-child(even){ + background-color: #f0f0f0; + } + + .label-column { + font-weight: bold; + } + + .filters { + display: flex; + align-items: center; + padding-top: 10px; + } + + .first-field { + margin-right: auto; + } + + .right-align { + margin-left: 16px; } } diff --git a/web/src/app/allocations-drawer/allocations-drawer.component.spec.ts b/web/src/app/allocations-drawer/allocations-drawer.component.spec.ts index ba0786f1..64ffbbce 100644 --- a/web/src/app/allocations-drawer/allocations-drawer.component.spec.ts +++ b/web/src/app/allocations-drawer/allocations-drawer.component.spec.ts @@ -18,6 +18,8 @@ import { MockSchedulerService, } from '@app/testing/mocks'; import {EnvConfigService} from '@app/services/envconfig/envconfig.service'; +import { MAT_FORM_FIELD_DEFAULT_OPTIONS, MatFormFieldModule } from "@angular/material/form-field"; +import { MatDialogModule } from "@angular/material/dialog"; describe("AllocationsDrawerComponent", () => { let component: AllocationsDrawerComponent; @@ -35,10 +37,13 @@ describe("AllocationsDrawerComponent", () => { MatInputModule, MatTableModule, MatSelectModule, + MatFormFieldModule, + MatDialogModule, ], providers: [ { provide: EnvConfigService, useValue: MockEnvconfigService }, - { provide: NgxSpinnerService, useValue: MockNgxSpinnerService } + { provide: NgxSpinnerService, useValue: MockNgxSpinnerService }, + { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}} ], }); fixture = TestBed.createComponent(AllocationsDrawerComponent); diff --git a/web/src/app/allocations-drawer/allocations-drawer.component.ts b/web/src/app/allocations-drawer/allocations-drawer.component.ts index 644fc100..35e594cd 100644 --- a/web/src/app/allocations-drawer/allocations-drawer.component.ts +++ b/web/src/app/allocations-drawer/allocations-drawer.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Injectable, Input, OnInit, Output, ViewChild } from "@angular/core"; +import { FormControl } from "@angular/forms"; import { MatPaginator } from "@angular/material/paginator"; import { MatDrawer } from "@angular/material/sidenav"; import { MatSort } from "@angular/material/sort"; @@ -27,14 +28,34 @@ export class AllocationsDrawerComponent implements OnInit { @Output() removeRowSelection = new EventEmitter(); + + disableSelect = new FormControl(false); + selectedAllocation: any; + showDetails: boolean = false; allocColumnDef: ColumnDef[] = []; allocColumnIds: string[] = []; selectedAllocationsRow: number = -1; + displayedColumns: string[] = ['key', 'value']; + + dataSource = new MatTableDataSource<{ key: string, value: string|undefined }>([]); + + filteredDataSource = new MatTableDataSource([]); + + selectedState: string = ''; + selectedNode: string = ''; + selectedInstance: string = ''; + + states = ['Running', 'Unknown', 'Failed', 'Succeeded']; + nodes = ['lima-rancher-desktop', 'Custom name 1', 'Custom name 2', 'Custom name 3']; + instances: string[] = []; + ngOnChanges(): void { if (this.allocDataSource) { + this.updateInstances(); this.allocDataSource.paginator = this.allocPaginator; this.allocDataSource.sort = this.allocSort; + this.applyFilter(); } } constructor(private envConfig: EnvConfigService) {} @@ -42,25 +63,34 @@ export class AllocationsDrawerComponent implements OnInit { ngOnInit(): void { this.allocColumnDef = [ { colId: "displayName", colName: "Display Name", colWidth: 1 }, - { colId: "allocationKey", colName: "Allocation Key", colWidth: 1 }, + { colId: "resource", colName: "Resource", colWidth: 1, colFormatter: CommonUtil.resourceColumnFormatter }, { colId: "nodeId", colName: "Node ID", colWidth: 1 }, - { - colId: "log", - colName: "Log Link", - colWidth: 1, - }, - { - colId: "resource", - colName: "Resource", - colFormatter: CommonUtil.resourceColumnFormatter, - colWidth: 1, - }, - { colId: "priority", colName: "Priority", colWidth: 0.5 }, + { colId: "state", colName: "State", colWidth: 1 }, ]; this.allocColumnIds = this.allocColumnDef.map((col) => col.colId); this.externalLogsBaseUrl = this.envConfig.getExternalLogsBaseUrl(); } + applyFilter(): void { + this.filteredDataSource.data = this.allocDataSource.data.filter(item => { + /* + const matchesState = this.selectedState ? item.state === this.selectedState : true; + const matchesNode = this.selectedNode ? item.node === this.selectedNode : true; + const matchesInstance = this.selectedInstance ? item.instance === this.selectedInstance : true; + return matchesState && matchesNode && matchesInstance; + + */ + const matchesInstance = this.selectedInstance ? item.displayName === this.selectedInstance : true; + return matchesInstance; + }); + } + + updateInstances(): void { + if (this.allocDataSource && this.allocDataSource.data) { + this.instances = [...new Set(this.allocDataSource.data.map(item => item.displayName))]; + } + } + formatResources(colValue: string): string[] { const arr: string[] = colValue.split("
"); // Check if there are "cpu" or "Memory" elements in the array @@ -72,10 +102,10 @@ export class AllocationsDrawerComponent implements OnInit { if (!hasMemory) { 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; @@ -85,7 +115,23 @@ export class AllocationsDrawerComponent implements OnInit { return this.allocDataSource?.data && this.allocDataSource.data.length === 0; } - allocationsDetailToggle(row: number) { + allocationsDetailToggle(row: any) { + this.showDetails = true; + this.selectedAllocation = row; + const newData = [ + { key: 'User', value: undefined }, + { key: 'Name', value: undefined }, + { key: 'Application Type', value: "spark" }, + { key: 'Application Tags', value: undefined }, + { key: 'YarnApplication State', value: this.selectedRow?.applicationState }, + { key: 'FinalStatus Reported by AM', value: undefined }, + { key: 'Started', value: undefined }, + { key: 'Launched', value: undefined }, + { key: 'Finished', value: undefined }, + { key: 'Elapsed', value: undefined }, + ]; + + this.dataSource.data = newData; if (this.selectedAllocationsRow !== -1) { if (this.selectedAllocationsRow !== row) { this.allocDataSource.data[this.selectedAllocationsRow].expanded = false; @@ -101,6 +147,10 @@ export class AllocationsDrawerComponent implements OnInit { } } + goBackToTable(){ + this.showDetails = false; + } + closeDrawer() { this.selectedAllocationsRow = -1; this.matDrawer.close(); diff --git a/web/src/app/allocations-drawer/allocations-drawer.module.ts b/web/src/app/allocations-drawer/allocations-drawer.module.ts index 6933c0a4..fadda19c 100644 --- a/web/src/app/allocations-drawer/allocations-drawer.module.ts +++ b/web/src/app/allocations-drawer/allocations-drawer.module.ts @@ -2,15 +2,18 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { MatPaginatorModule } from "@angular/material/paginator"; import { MatSidenavModule } from "@angular/material/sidenav"; +import { MatIconModule } from '@angular/material/icon'; import { MatSortModule } from "@angular/material/sort"; import { MatTableModule } from "@angular/material/table"; import { MatTooltipModule } from "@angular/material/tooltip"; import { BrowserModule } from "@angular/platform-browser"; import { AllocationsDrawerComponent } from "./allocations-drawer.component"; +import { MatSelectModule } from "@angular/material/select"; +import { MatFormFieldModule } from "@angular/material/form-field"; @NgModule({ declarations: [AllocationsDrawerComponent], - imports: [CommonModule, MatSortModule, MatSidenavModule, MatPaginatorModule, MatTableModule, MatTooltipModule, BrowserModule], + imports: [CommonModule, MatSortModule, MatSidenavModule, MatPaginatorModule, MatTableModule, MatTooltipModule, MatIconModule, BrowserModule, MatFormFieldModule, MatSelectModule], exports: [AllocationsDrawerComponent], }) export class AllocationsDrawerModule {} diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts index 74da8f7a..80b54730 100644 --- a/web/src/app/app.module.ts +++ b/web/src/app/app.module.ts @@ -3,16 +3,17 @@ import { BrowserModule } from "@angular/platform-browser"; import { envConfigFactory, EnvConfigService } from "@app/services/envconfig/envconfig.service"; import { AllocationsDrawerModule } from "./allocations-drawer/allocations-drawer.module"; import { AppComponent } from "./app.component"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @NgModule({ declarations: [AppComponent], - imports: [BrowserModule, AllocationsDrawerModule], + imports: [BrowserModule, AllocationsDrawerModule, BrowserAnimationsModule], providers: [ { useFactory: envConfigFactory, provide: EnvConfigService, deps: [EnvConfigService], - } + }, ], bootstrap: [AppComponent], }) diff --git a/web/src/app/apps-view/apps-view.component.html b/web/src/app/apps-view/apps-view.component.html new file mode 100644 index 00000000..53cea73f --- /dev/null +++ b/web/src/app/apps-view/apps-view.component.html @@ -0,0 +1,146 @@ + + +
+
+ + +
+
+ + + + + +
+ +
+
+
+
+
+ + + {{ columnDef.colName + }} + + + {{ + element['formattedSubmissionTime'] }} + + + + {{ + element['formattedlastStateChangeTime'] }} + + + + + + + + + + + {{ resource }} + + + {{ resource }} + + + + + + + {{ element[columnDef.colId] }} + + + + + + + + + + + + + + + {{ element[columnDef.colId] || 'n/a' }} + + + + + + + + + + + + + + + +
No records found
+
+
+ + + + + + +
+ + +
+ + + + +
\ No newline at end of file diff --git a/web/src/app/apps-view/apps-view.component.scss b/web/src/app/apps-view/apps-view.component.scss new file mode 100644 index 00000000..5b1d5fdf --- /dev/null +++ b/web/src/app/apps-view/apps-view.component.scss @@ -0,0 +1,272 @@ +/** + * 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 '~material-design-icons/iconfont/material-icons.css'; + + .top-section { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + .left-side{ + display: flex; + flex-direction: row; + align-items: center; + .dropdown-wrapper { + padding-right: 40px; + .dropdown-label { + color: #333; + font-size: 1.2em; + margin-right: 10px; + } + } + } + .right-side{ + display: flex; + flex-direction: row; + align-items: center; + .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); + } + } + .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; + } + } + } +} + +.mdc-button .mdc-button__label{ + display: flex; + align-items: center; +} + +.apps-view { + width: 100%; + height: 100%; + + mat-chip { + height: 20px; + line-height: 14px; + font-size: 12px; + margin: 1px; + // border-radius: 5px; + // background-color: #313D54; + background-color: transparent; + + span { + font-size: 12px; + } + } + + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #333; + --mdc-chip-label-text-size: 12px; + --mdc-chip-label-text-weight: 400; + } + + .mat-mdc-header-cell { + font-size: 15px; + font-weight: bold; + color: #666; + } + .mat-mdc-cell { + color: #333; + display: flex; + flex-direction: column; + align-items: flex-start; + .mat-res-ul{ + padding: 0; + margin: 0; + .mat-res-li{ + list-style-type: none; + } + } + .mat-toggle-more { + display: block; + color: #a1a3b7; + padding: 5px 45px 0 0; + &:hover { + color: #8d00d4; + } + } + } + .even-row { + background: #eee; + } + .mat-mdc-row { + min-height: unset; + font-size: 12px; + &:hover { + background: #cccccc; + cursor: pointer; + } + &.selected-row { + background: #303d54; + .mat-mdc-cell { + color: #fff; + .mat-toggle-more { + color: #b8bbff; + &:hover { + color: #b871dc; + } + } + } + } + } + .mat-mdc-header-cell.indicator-icon, + .mat-mdc-cell.indicator-icon { + max-width: 40px; + font-size: 18px; + margin-left: 10px; + } + .app-allocations { + margin-top: 40px; + .mat-mdc-table { + margin-top: 20px; + } + } + .no-record { + font-size: 14px; + font-weight: bold; + color: #666; + width: 100%; + text-align: center; + } +} + + +.mat-drawer-container { + min-width: 430px; + width: 55%; + height: calc(100vh - 60px); + background: transparent; + pointer-events: none; + position: absolute; + right: 0; + bottom: 0; + .mat-drawer { + pointer-events: auto; + width: 100%; + .content { + padding: 0 10px; + } + } + .close-btn { + float: right; + font-size: 1.2em; + cursor: pointer; + padding-right: 5px; + &:hover { + color: #f44336; + } + } + .copy-btn { + font-size: 1em; + cursor: pointer; + padding-left: 5px; + } + .header { + margin: 20px; + font-weight: 100; + font-size: 1em; + } + .content { + border-top: 1px solid #e1e1e1; + } + .item-wrapper { + .left-item { + text-align: right; + } + .left-item, + .right-item { + width: 50%; + padding: 6px; + padding-right: 0; + color: #666; + } + .right-item { + font-weight: 600; + } + } + .app-link { + text-decoration: none; + color: #666; + &:hover { + text-decoration: underline; + } + } + + .row { + font-size: 0.9em; + } + + .even-row { + background: #eee; + } + + .ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + } + + .row { + min-height: unset; + } +} \ No newline at end of file diff --git a/web/src/app/apps-view/apps-view.component.spec.ts b/web/src/app/apps-view/apps-view.component.spec.ts new file mode 100644 index 00000000..b9e667f8 --- /dev/null +++ b/web/src/app/apps-view/apps-view.component.spec.ts @@ -0,0 +1,143 @@ +/** + * 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 { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatInputModule } from '@angular/material/input'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { By, HAMMER_LOADER } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppInfo } from '@app/models/app-info.model'; +import { EnvConfigService } from '@app/services/envconfig/envconfig.service'; +import { SchedulerService } from '@app/services/scheduler/scheduler.service'; +import { MatChipsModule } from '@angular/material/chips'; + +import { + MockEnvconfigService, + MockNgxSpinnerService, + MockSchedulerService, +} from '@app/testing/mocks'; +import { NgxSpinnerService } from 'ngx-spinner'; +import { of } from 'rxjs'; + +import { AppsViewComponent } from './apps-view.component'; + +describe('AppsViewComponent', () => { + let component: AppsViewComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AppsViewComponent], + imports: [ + NoopAnimationsModule, + RouterTestingModule, + FormsModule, + MatTableModule, + MatPaginatorModule, + MatDividerModule, + MatSortModule, + MatInputModule, + MatTooltipModule, + MatSelectModule, + MatSidenavModule, + MatChipsModule, + ], + providers: [ + { provide: SchedulerService, useValue: MockSchedulerService }, + { provide: NgxSpinnerService, useValue: MockNgxSpinnerService }, + { provide: HAMMER_LOADER, useValue: () => new Promise(() => {}) }, + { provide: EnvConfigService, useValue: MockEnvconfigService }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(AppsViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should have usedResource and pendingResource column with detailToggle OFF', () => { + let service: SchedulerService; + service = TestBed.inject(SchedulerService); + let appInfo = new AppInfo( + 'app1', + 'Memory: 500.0 KB, CPU: 10, pods: 1', + 'Memory: 0.0 bytes, CPU: 0, pods: n/a', + '', + 1, + 2, + [], + 2, + 'RUNNING', + [] + ); + spyOn(service, 'fetchAppList').and.returnValue(of([appInfo])); + component.fetchAppListForPartitionAndQueue('default', 'root'); + fixture.detectChanges(); + const debugEl: DebugElement = fixture.debugElement; + expect( + debugEl.query(By.css('[data-test="Memory: 500.0 KB,CPU: 10,pods: 1"]')).nativeElement + .innerText + ).toContain('Memory: 500.0 KB\nCPU: 10'); + expect( + debugEl.query(By.css('[data-test="Memory: 0.0 bytes,CPU: 0,pods: n/a"]')).nativeElement + .innerText + ).toContain('Memory: 0.0 bytes\nCPU: 0'); + }); + + it('should have usedResource and pendingResource column with detailToggle ON', () => { + let service: SchedulerService; + service = TestBed.inject(SchedulerService); + let appInfo = new AppInfo( + 'app1', + 'Memory: 500.0 KB, CPU: 10, pods: 1', + 'Memory: 0.0 bytes, CPU: 0, pods: n/a', + '', + 1, + 2, + [], + 2, + 'RUNNING', + [] + ); + spyOn(service, 'fetchAppList').and.returnValue(of([appInfo])); + component.fetchAppListForPartitionAndQueue('default', 'root'); + component.detailToggle = true; + fixture.detectChanges(); + const debugEl: DebugElement = fixture.debugElement; + expect( + debugEl.query(By.css('[data-test="Memory: 500.0 KB,CPU: 10,pods: 1"]')).nativeElement + .innerText + ).toContain('Memory: 500.0 KB\nCPU: 10\npods: 1'); + expect( + debugEl.query(By.css('[data-test="Memory: 0.0 bytes,CPU: 0,pods: n/a"]')).nativeElement + .innerText + ).toContain('Memory: 0.0 bytes\nCPU: 0\npods: n/a'); + }); +}); diff --git a/web/src/app/apps-view/apps-view.component.ts b/web/src/app/apps-view/apps-view.component.ts new file mode 100644 index 00000000..f70ed9ae --- /dev/null +++ b/web/src/app/apps-view/apps-view.component.ts @@ -0,0 +1,418 @@ +/** + * 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, + ComponentRef, + ElementRef, + OnInit, + ViewChild, + ViewContainerRef, + } from '@angular/core'; + import { MatPaginator } from '@angular/material/paginator'; + import { MatSelect, MatSelectChange } from '@angular/material/select'; + import { MatSort } from '@angular/material/sort'; + import { MatTableDataSource } from '@angular/material/table'; + import { ActivatedRoute, Router } from '@angular/router'; + import { AllocationsDrawerComponent } from '@app/allocations-drawer/allocations-drawer.component'; + import { AllocationInfo } from '@app/models/alloc-info.model'; + import { AppInfo } from '@app/models/app-info.model'; + import { ColumnDef } from '@app/models/column-def.model'; + import { DropdownItem } from '@app/models/dropdown-item.model'; + import { PartitionInfo } from '@app/models/partition-info.model'; + import { QueueInfo } from '@app/models/queue-info.model'; + import { EnvConfigService } from '@app/services/envconfig/envconfig.service'; + import { SchedulerService } from '@app/services/scheduler/scheduler.service'; + import { CommonUtil } from '@app/utils/common.util'; + import { NgxSpinnerService } from 'ngx-spinner'; + import { fromEvent } from 'rxjs'; + import { debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators'; + import { + loadRemoteModule, + LoadRemoteModuleEsmOptions, + } from '@angular-architects/module-federation'; + import { SchedulerServiceLoader } from '@app/services/scheduler/scheduler-loader.service'; + + @Component({ + selector: 'app-applications-view', + templateUrl: './apps-view.component.html', + styleUrls: ['./apps-view.component.scss'], + }) + export class AppsViewComponent implements OnInit { + @ViewChild('appsViewMatPaginator', { static: true }) appPaginator!: MatPaginator; + @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('drawerContainer', { read: ViewContainerRef, static: true }) + drawerContainer!: ViewContainerRef; + + @ViewChild('mfeContainer', { read: ViewContainerRef, static: true }) + mfeContainer!: ViewContainerRef; + + appDataSource = new MatTableDataSource([]); + appColumnDef: ColumnDef[] = []; + appColumnIds: string[] = []; + + allocDataSource = new MatTableDataSource([]); + allocColumnDef: ColumnDef[] = []; + allocColumnIds: string[] = []; + + selectedRow: AppInfo | null = null; + initialAppData: AppInfo[] = []; + searchText = ''; + partitionList: PartitionInfo[] = []; + partitionSelected = ''; + leafQueueList: DropdownItem[] = []; + leafQueueSelected = ''; + + detailToggle: boolean = false; + allocationsDrawerComponent: ComponentRef | undefined = undefined; + + constructor( + private scheduler: SchedulerService, + private spinner: NgxSpinnerService, + private activatedRoute: ActivatedRoute, + private router: Router, + private envConfig: EnvConfigService + ) {} + + ngOnInit() { + this.appDataSource.paginator = this.appPaginator; + this.allocDataSource.paginator = this.allocPaginator; + this.appDataSource.sort = this.appSort; + this.allocDataSource.sort = this.allocSort; + this.appSort.sort({ id: 'submissionTime', start: 'desc', disableClear: false }); + + this.appColumnDef = [ + { colId: 'applicationId', colName: 'Application ID', colWidth: 1 }, + { colId: 'applicationState', colName: 'Application State', colWidth: 1 }, + { + colId: 'lastStateChangeTime', + colName: 'Last State Change Time', + colFormatter: CommonUtil.timeColumnFormatter, + colWidth: 1, + }, + { + colId: 'usedResource', + colName: 'Used Resource', + colFormatter: CommonUtil.resourceColumnFormatter, + colWidth: 2, + }, + { + colId: 'pendingResource', + colName: 'Pending Resource', + colFormatter: CommonUtil.resourceColumnFormatter, + colWidth: 2, + }, + { + colId: 'submissionTime', + colName: 'Submission Time', + colFormatter: CommonUtil.timeColumnFormatter, + colWidth: 1, + }, + ]; + + this.appColumnIds = this.appColumnDef.map((col) => col.colId); + + this.allocColumnDef = [ + { colId: 'displayName', colName: 'Display Name', colWidth: 1 }, + { colId: 'allocationKey', colName: 'Allocation Key', colWidth: 1 }, + { colId: 'nodeId', colName: 'Node ID', colWidth: 1 }, + { + colId: 'resource', + colName: 'Resource', + colFormatter: CommonUtil.resourceColumnFormatter, + colWidth: 1, + }, + { colId: 'priority', colName: 'Priority', colWidth: 0.5 }, + ]; + + this.allocColumnIds = this.allocColumnDef.map((col) => col.colId); + + fromEvent(this.searchInput.nativeElement, 'keyup') + .pipe(debounceTime(500), distinctUntilChanged()) + .subscribe(() => { + this.onSearchAppData(); + }); + + this.scheduler + .fetchPartitionList() + .pipe( + finalize(() => { + this.spinner.hide(); + }) + ) + .subscribe((list) => { + if (list && list.length > 0) { + list.forEach((part) => { + this.partitionList.push(new PartitionInfo(part.name, part.name)); + }); + this.partitionSelected = CommonUtil.getStoredPartition(list[0].name); + this.fetchQueuesForPartition(this.partitionSelected); + } else { + this.partitionList = [new PartitionInfo('-- Select --', '')]; + this.partitionSelected = ''; + this.leafQueueList = [new DropdownItem('-- Select --', '')]; + this.leafQueueSelected = ''; + this.appDataSource.data = []; + this.clearQueueSelection(); + } + }); + + } + + fetchQueuesForPartition(partitionName: string) { + this.spinner.show(); + + this.scheduler + .fetchSchedulerQueues(partitionName) + .pipe( + finalize(() => { + this.spinner.hide(); + }) + ) + .subscribe((data) => { + if (data && data.rootQueue) { + const leafQueueList = this.generateLeafQueueList(data.rootQueue); + this.leafQueueList = [new DropdownItem('-- Select --', ''), ...leafQueueList]; + if (!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)); + } + + if (rootQueue && rootQueue.children) { + rootQueue.children.forEach((child) => this.generateLeafQueueList(child, list)); + } + + return list; + } + + fetchAppListForPartitionAndQueue( + partitionName: string, + queueName: string, + applicationId?: string + ) { + this.spinner.show(); + + this.scheduler + .fetchAppList(partitionName, queueName) + .pipe( + finalize(() => { + this.spinner.hide(); + }) + ) + .subscribe((data) => { + this.initialAppData = data; + this.appDataSource.data = data; + + const row = this.initialAppData.find((app) => app.applicationId === applicationId); + if (row) { + this.toggleRowSelection(row); + } + }); + } + + fetchApplicationsUsingQueryParams(): boolean { + const partitionName = this.activatedRoute.snapshot.queryParams['partition']; + const queueName = this.activatedRoute.snapshot.queryParams['queue']; + const applicationId = this.activatedRoute.snapshot.queryParams['applicationId']; + + if (partitionName && queueName) { + this.partitionSelected = partitionName; + this.leafQueueSelected = queueName; + this.fetchAppListForPartitionAndQueue(partitionName, queueName, applicationId); + CommonUtil.setStoredQueueAndPartition(partitionName, queueName); + + this.router.navigate([], { + queryParams: { + partition: null, + queue: null, + applicationId: null, + }, + queryParamsHandling: 'merge', + }); + + return true; + } + return false; + } + + unselectAllRowsButOne(row: AppInfo) { + this.appDataSource.data.map((app) => { + if (app !== row) { + app.isSelected = false; + } + }); + } + + toggleRowSelection(row: AppInfo) { + this.unselectAllRowsButOne(row); + if (row.isSelected) { + this.removeRowSelection(); + } else { + this.selectedRow = row; + row.isSelected = true; + if (row.allocations) { + this.allocDataSource.data = row.allocations; + } + this.allocationsDrawerComponent?.setInput('selectedRow', this.selectedRow); + this.allocationsDrawerComponent?.setInput('allocDataSource', this.allocDataSource); + this.allocationsDrawerComponent?.instance.openDrawer(); + } + } + + removeRowSelection() { + if (this.selectedRow) { + this.selectedRow.isSelected = false; + this.selectedRow = null; + this.allocDataSource.data = []; + } + } + + onPaginatorChanged() { + this.removeRowSelection(); + } + + isAppDataSourceEmpty() { + return this.appDataSource.data && this.appDataSource.data.length === 0; + } + + onClearSearch() { + this.searchText = ''; + this.removeRowSelection(); + this.appDataSource.data = this.initialAppData; + } + + onSearchAppData() { + const searchTerm = this.searchText.trim().toLowerCase(); + + if (searchTerm) { + this.removeRowSelection(); + this.appDataSource.data = this.initialAppData.filter((data) => + data.applicationId.toLowerCase().includes(searchTerm) + ); + } else { + this.onClearSearch(); + } + } + + onPartitionSelectionChanged(selected: MatSelectChange) { + if (selected.value !== '') { + this.searchText = ''; + this.partitionSelected = selected.value; + this.appDataSource.data = []; + this.removeRowSelection(); + this.clearQueueSelection(); + this.fetchQueuesForPartition(this.partitionSelected); + } else { + this.searchText = ''; + this.partitionSelected = ''; + this.leafQueueSelected = ''; + this.appDataSource.data = []; + this.removeRowSelection(); + this.clearQueueSelection(); + } + } + + onQueueSelectionChanged(selected: MatSelectChange) { + if (selected.value !== '') { + this.searchText = ''; + this.leafQueueSelected = selected.value; + 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('
'); + // 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')); + if (!hasCpu) { + arr.unshift('CPU: n/a'); + } + if (!hasMemory) { + 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') + ); + return cpuAndMemoryElements.concat(otherElements); + } + + clearQueueSelection() { + CommonUtil.setStoredQueueAndPartition(''); + this.leafQueueSelected = ''; + this.openQueueSelection(); + } + + openQueueSelection() { + this.queueSelect.open(); + } + + toggle() { + this.detailToggle = !this.detailToggle; + } + } + \ No newline at end of file diff --git a/web/src/app/apps-view/apps-view.module.ts b/web/src/app/apps-view/apps-view.module.ts new file mode 100644 index 00000000..2d7cf361 --- /dev/null +++ b/web/src/app/apps-view/apps-view.module.ts @@ -0,0 +1,10 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { AppsViewComponent } from "./apps-view.component"; + +@NgModule({ + declarations: [AppsViewComponent], + imports: [CommonModule], + exports: [AppsViewComponent], +}) +export class AppsViewModule {} diff --git a/web/src/app/models/dropdown-item.model.ts b/web/src/app/models/dropdown-item.model.ts new file mode 100644 index 00000000..83300504 --- /dev/null +++ b/web/src/app/models/dropdown-item.model.ts @@ -0,0 +1,28 @@ +/** + * 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. + */ + +export class DropdownItem { + name: string; + value: string; + + constructor(name: string, value: string) { + this.name = name; + this.value = value; + } + } + \ No newline at end of file diff --git a/web/src/app/services/envconfig/envconfig.service.ts b/web/src/app/services/envconfig/envconfig.service.ts index 579e14de..d8626705 100644 --- a/web/src/app/services/envconfig/envconfig.service.ts +++ b/web/src/app/services/envconfig/envconfig.service.ts @@ -42,7 +42,7 @@ export class EnvConfigService { this.uiHostname = window.location.hostname; this.uiPort = window.location.port; this.envConfig = { - localYhsComponentsWebAddress: "http://localhost:3000", + localYhsComponentsWebAddress: "http://localhost:30001", }; } diff --git a/web/src/app/services/scheduler/scheduler-loader.service.ts b/web/src/app/services/scheduler/scheduler-loader.service.ts new file mode 100644 index 00000000..0e078dbc --- /dev/null +++ b/web/src/app/services/scheduler/scheduler-loader.service.ts @@ -0,0 +1,28 @@ +import { Injectable, Injector } from '@angular/core'; +import { loadRemoteModule, LoadRemoteModuleEsmOptions } from '@angular-architects/module-federation'; +import { SchedulerService } from './scheduler.service'; + +@Injectable({ + providedIn: 'root', +}) +export class SchedulerServiceLoader { + constructor(private injector: Injector) {} + + async initializeSchedulerService(remoteComponentConfig: LoadRemoteModuleEsmOptions | null): Promise { + if (remoteComponentConfig !== null) { + try { + const remoteModule = await loadRemoteModule(remoteComponentConfig); + if (remoteModule && remoteModule.SchedulerService) { + return this.injector.get(remoteModule.SchedulerService); + } else { + console.error('SchedulerService not found.'); + return null; + } + } catch (error) { + console.error('Error loading the remote module:', error); + return null; + } + } + return null; + } +} \ No newline at end of file diff --git a/web/webpack.config.js b/web/webpack.config.js index d6d33a44..f1db93f2 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -48,6 +48,9 @@ module.exports = { "@angular/material/table": {singleton:true, strictVersion: true, requiredVersion: 'auto'}, "@angular/material/tooltip": {singleton:true, strictVersion: true, requiredVersion: 'auto'}, "@angular/platform-browser": {singleton:true, strictVersion: true, requiredVersion: 'auto'}, + "@angular/material/select": {singleton:true, strictVersion: true, requiredVersion: 'auto'}, + "@angular/material/form-field": {singleton:true, strictVersion: true, requiredVersion: 'auto'}, + "@angular/material/core": {singleton:true, strictVersion: true, requiredVersion: 'auto'}, ...sharedMappings.getDescriptors(), }) diff --git a/web/yarn.lock b/web/yarn.lock index 53342bcd..570904d8 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2690,7 +2690,7 @@ base64id@2.0.0, base64id@~2.0.0: basic-auth@~2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== dependencies: safe-buffer "5.1.2" @@ -3043,7 +3043,7 @@ connect-history-api-fallback@^2.0.0: connect-pause@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/connect-pause/-/connect-pause-0.1.1.tgz#b269b2bb82ddb1ac3db5099c0fb582aba99fb37a" + resolved "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz" integrity sha512-a1gSWQBQD73krFXdUEYJom2RTFrWUL3YvXDCRkyv//GVXc79cdW9MngtRuN9ih4FDKBtfJAJId+BbDuX+1rh2w== connect@^3.7.0: @@ -3225,7 +3225,7 @@ debug@2.6.9: debug@3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" @@ -3271,11 +3271,6 @@ define-lazy-prop@^3.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== -depd@2.0.0, depd@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -3472,7 +3467,7 @@ error-ex@^1.3.1: errorhandler@^1.5.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.5.1.tgz#b9ba5d17cf90744cd1e851357a6e75bf806a9a91" + resolved "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz" integrity sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A== dependencies: accepts "~1.3.7" @@ -3646,7 +3641,7 @@ exponential-backoff@^3.1.1: express-urlrewrite@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz#985ee022773bac7ed32126f1cf9ec8ee48e1290a" + resolved "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz" integrity sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA== dependencies: debug "*" @@ -4340,7 +4335,7 @@ is-plain-object@^2.0.4: is-promise@^2.1.0: version "2.2.2" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== is-stream@^2.0.0: @@ -4365,16 +4360,16 @@ is-wsl@^3.1.0: dependencies: is-inside-container "^1.0.0" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" @@ -4483,7 +4478,7 @@ jiti@^1.20.0: jju@^1.1.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" + resolved "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== js-tokens@^4.0.0: @@ -4525,7 +4520,7 @@ json-parse-even-better-errors@^3.0.0: json-parse-helpfulerror@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz#13f14ce02eed4e981297b64eb9e3b932e2dd13dc" + resolved "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz" integrity sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg== dependencies: jju "^1.1.0" @@ -4542,7 +4537,7 @@ json-schema-traverse@^1.0.0: json-server@^0.17.4: version "0.17.4" - resolved "https://registry.yarnpkg.com/json-server/-/json-server-0.17.4.tgz#d4ef25a516e26d9ba86fd6db2f9d81a5f405421e" + resolved "https://registry.npmjs.org/json-server/-/json-server-0.17.4.tgz" integrity sha512-bGBb0WtFuAKbgI7JV3A864irWnMZSvBYRJbohaOuatHwKSRFUfqtQlrYMrB6WbalXy/cJabyjlb7JkHli6dYjQ== dependencies: body-parser "^1.19.0" @@ -4760,7 +4755,7 @@ locate-path@^7.1.0: lodash-id@^0.14.1: version "0.14.1" - resolved "https://registry.yarnpkg.com/lodash-id/-/lodash-id-0.14.1.tgz#dffa1f1f8b90d1803bb0d70b7d7547e10751e80b" + resolved "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz" integrity sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg== lodash.debounce@^4.0.8: @@ -4805,7 +4800,7 @@ log4js@^6.4.1: lowdb@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" + resolved "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz" integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== dependencies: graceful-fs "^4.1.3" @@ -4898,7 +4893,7 @@ merge2@^1.3.0: method-override@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2" + resolved "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz" integrity sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA== dependencies: debug "3.1.0" @@ -5071,7 +5066,7 @@ moment@^2.30.1: morgan@^1.10.0: version "1.10.0" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== dependencies: basic-auth "~2.0.1" @@ -5591,7 +5586,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: pify@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== pify@^4.0.1: @@ -5615,14 +5610,14 @@ pkg-dir@^7.0.0: please-upgrade-node@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + resolved "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz" integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== dependencies: semver-compare "^1.0.0" pluralize@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== postcss-loader@8.1.1: @@ -6099,7 +6094,7 @@ selfsigned@^2.4.1: semver-compare@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== semver@7.6.3, semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: @@ -6168,7 +6163,7 @@ serve-static@1.16.2: server-destroy@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd" + resolved "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz" integrity sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ== set-function-length@^1.2.1: @@ -6432,9 +6427,14 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + steno@^0.4.1: version "0.4.4" - resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" + resolved "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz" integrity sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w== dependencies: graceful-fs "^4.1.3" @@ -6496,7 +6496,7 @@ string_decoder@~1.1.1: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: - safe-buffer "~5.1.0" + ansi-regex "^5.0.1" "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" @@ -7110,19 +7110,6 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@17.7.2, yargs@^17.0.1, yargs@^17.2.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yargs@^16.1.1: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"