diff --git a/openapitools.json b/openapitools.json new file mode 100644 index 00000000..7f8d0939 --- /dev/null +++ b/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "6.2.0" + } +} diff --git a/package-lock.json b/package-lock.json index acbbdb8e..77901fcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,8 @@ "depcheck": "^1.4.2", "domsanitizer": "^0.2.3", "monaco-editor": "^0.34.1", + "ng-for-track-by-property": "^14.0.0", + "ng-table-virtual-scroll": "~1.3.8", "ngx-clipboard": "^15.1.0", "oidc-client": "^1.11.5", "rxjs": "^6.6.7", @@ -57,6 +59,7 @@ "@types/jasminewd2": "^2.0.10", "@types/node": "^16.4.11", "codelyzer": "^6.0.2", + "cross-env": "^7.0.3", "cross-var": "^1.1.0", "jasmine-core": "^3.8.0", "jasmine-spec-reporter": "^7.0.0", @@ -68,6 +71,7 @@ "ng-packagr": "^14.2.2", "prettier": "^2.7.1", "protractor": "^7.0.0", + "rimraf": "^3.0.2", "rxjs-tslint-rules": "^4.34.8", "rxjs-watcher": "^1.1.3", "ts-node": "^10.1.0", @@ -7740,6 +7744,74 @@ "node": ">=8" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-env/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-env/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -12394,6 +12466,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ng-for-track-by-property": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/ng-for-track-by-property/-/ng-for-track-by-property-14.0.2.tgz", + "integrity": "sha512-GtVUx9LeYnWlPFKUqbKSuni2y3PnkWkJonyrpmlk8XYM8sIVBSO6QclXoXqVYSXGW3xOnoQyc2t6kabsx5yj3A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^14.0.0", + "@angular/core": "^14.0.0" + } + }, "node_modules/ng-packagr": { "version": "14.2.2", "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-14.2.2.tgz", @@ -12498,6 +12582,20 @@ "tslib": "^2.1.0" } }, + "node_modules/ng-table-virtual-scroll": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ng-table-virtual-scroll/-/ng-table-virtual-scroll-1.3.8.tgz", + "integrity": "sha512-8FvAQ0INvWLKHxxsw9X7GRbOrW45+2GK2G/UIHT2b0weZGvZcgtwzpC9ipCfuVngw+cW61m1B9fU4Cfej2bF2A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/cdk": ">=6.0.0", + "@angular/common": ">=6.0.0", + "@angular/core": ">=6.0.0", + "@angular/material": ">=6.0.0" + } + }, "node_modules/ngx-clipboard": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/ngx-clipboard/-/ngx-clipboard-15.1.0.tgz", @@ -23977,6 +24075,52 @@ } } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -27483,6 +27627,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "ng-for-track-by-property": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/ng-for-track-by-property/-/ng-for-track-by-property-14.0.2.tgz", + "integrity": "sha512-GtVUx9LeYnWlPFKUqbKSuni2y3PnkWkJonyrpmlk8XYM8sIVBSO6QclXoXqVYSXGW3xOnoQyc2t6kabsx5yj3A==", + "requires": { + "tslib": "^2.3.0" + } + }, "ng-packagr": { "version": "14.2.2", "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-14.2.2.tgz", @@ -27564,6 +27716,14 @@ } } }, + "ng-table-virtual-scroll": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ng-table-virtual-scroll/-/ng-table-virtual-scroll-1.3.8.tgz", + "integrity": "sha512-8FvAQ0INvWLKHxxsw9X7GRbOrW45+2GK2G/UIHT2b0weZGvZcgtwzpC9ipCfuVngw+cW61m1B9fU4Cfej2bF2A==", + "requires": { + "tslib": "^2.0.0" + } + }, "ngx-clipboard": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/ngx-clipboard/-/ngx-clipboard-15.1.0.tgz", diff --git a/package.json b/package.json index d27cff42..543fe7ba 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "docker:destroy": "// Destroys the docker stack, data retained" }, "config": { - "port": "4310" + "port": "4310", + "openapiArgs": "--additional-properties ngVersion=14.2 --additional-properties modelPropertyNaming=original", + "deleteGeneratedCommand": "rimraf src/app/generated/caster-api/*" }, "scripts": { "ng": "ng", @@ -22,8 +24,8 @@ "lint": "ng lint caster-ui", "e2e": "ng e2e caster-ui", "postinstall": "ngcc", - "swagger:gen": "curl --insecure http://localhost:4309/swagger/v1/swagger.json --output swagger.json && node_modules/@openapitools/openapi-generator-cli/bin/openapi-generator generate -i ./swagger.json -g typescript-angular -o src/app/generated/caster-api --additional-properties ngVersion=12.1 --additional-properties useRxJS6=true --additional-properties modelPropertyNaming=original --skip-validate-spec", - "swagger:gen-win": "docker run --rm -v %CD%:/local openapitools/openapi-generator-cli:v5.3.0 generate -i http://host.docker.internal:4309/swagger/v1/swagger.json?format=openapi -g typescript-angular -o /local/src/app/generated/caster-api --additional-properties ngVersion=12.1 --additional-properties useRxJS6=true --additional-properties modelPropertyNaming=original --skip-validate-spec" + "swagger:gen": "cross-var $npm_package_config_deleteGeneratedCommand && cross-var curl --insecure http://localhost:4309/swagger/v1/swagger.json --output swagger.json && node_modules/@openapitools/openapi-generator-cli/bin/openapi-generator-cli generate -i ./swagger.json -g typescript-angular -o src/app/generated/caster-api $npm_package_config_openapiArgs", + "swagger:gen-docker": "cross-var $npm_package_config_deleteGeneratedCommand && cross-env-shell docker run --rm -v $INIT_CWD:/local openapitools/openapi-generator-cli:v6.2.0 generate -i http://host.docker.internal:4309/swagger/v1/swagger.json?format=openapi -g typescript-angular -o /local/src/app/generated/caster-api $npm_package_config_openapiArgs" }, "private": true, "dependencies": { @@ -55,6 +57,8 @@ "depcheck": "^1.4.2", "domsanitizer": "^0.2.3", "monaco-editor": "^0.34.1", + "ng-for-track-by-property": "^14.0.0", + "ng-table-virtual-scroll": "~1.3.8", "ngx-clipboard": "^15.1.0", "oidc-client": "^1.11.5", "rxjs": "^6.6.7", @@ -75,6 +79,7 @@ "@types/jasminewd2": "^2.0.10", "@types/node": "^16.4.11", "codelyzer": "^6.0.2", + "cross-env": "^7.0.3", "cross-var": "^1.1.0", "jasmine-core": "^3.8.0", "jasmine-spec-reporter": "^7.0.0", @@ -86,6 +91,7 @@ "ng-packagr": "^14.2.2", "prettier": "^2.7.1", "protractor": "^7.0.0", + "rimraf": "^3.0.2", "rxjs-tslint-rules": "^4.34.8", "rxjs-watcher": "^1.1.3", "ts-node": "^10.1.0", diff --git a/src/app/admin-app/admin-app-routing.module.ts b/src/app/admin-app/admin-app-routing.module.ts new file mode 100644 index 00000000..80b38b94 --- /dev/null +++ b/src/app/admin-app/admin-app-routing.module.ts @@ -0,0 +1,20 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AdminContainerComponent } from './component/admin-container/admin-container.component'; + +const routes: Routes = [ + { + path: '', + component: AdminContainerComponent, + pathMatch: 'full', + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AdminAppRoutingModule {} diff --git a/src/app/admin-app/admin-app.module.ts b/src/app/admin-app/admin-app.module.ts index 810ded16..61fca1ad 100644 --- a/src/app/admin-app/admin-app.module.ts +++ b/src/app/admin-app/admin-app.module.ts @@ -8,7 +8,6 @@ import { AdminContainerComponent } from './component/admin-container/admin-conta import { UsersComponent } from './component/admin-users/users.component'; import { UserListComponent } from './component/admin-users/user-list/user-list.component'; import { FlexLayoutModule } from '@angular/flex-layout'; -import { ProjectModule } from '../project/project.module'; import { RouterModule } from '@angular/router'; import { ClipboardModule } from 'ngx-clipboard'; import { AdminModuleListComponent } from './component/admin-modules/modules-list/module-list.component'; @@ -35,6 +34,22 @@ import { LockingStatusComponent } from './component/admin-workspaces/locking-sta import { ActiveRunsComponent } from './component/admin-workspaces/active-runs/active-runs.component'; import { CwdTableModule } from '../sei-cwd-common/cwd-table/cwd-table.module'; import { WorkspaceModule } from '../workspace/workspace.module'; +import { VlansComponent } from './component/admin-vlans/vlans.component'; +import { PoolListComponent } from './component/admin-vlans/pool-list/pool-list.component'; +import { PartitionListComponent } from './component/admin-vlans/partition-list/partition-list.component'; +import { PartitionComponent } from './component/admin-vlans/partition/partition.component'; +import { MatSliderModule } from '@angular/material/slider'; +import { VlanListComponent } from './component/admin-vlans/vlan-list/vlan-list.component'; +import { MatTableModule } from '@angular/material/table'; +import { TableVirtualScrollModule } from 'ng-table-virtual-scroll'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { MatSelectModule } from '@angular/material/select'; +import { PoolsComponent } from './component/admin-vlans/pools/pools.component'; +import { ProjectVlansComponent } from './component/admin-vlans/project-vlans/project-vlans.component'; +import { MatTabsModule } from '@angular/material/tabs'; +import { PoolListItemComponent } from './component/admin-vlans/pool-list-item/pool-list-item.component'; +import { AdminAppRoutingModule } from './admin-app-routing.module'; +import { SharedModule } from '../shared/shared.module'; @NgModule({ declarations: [ @@ -46,13 +61,27 @@ import { WorkspaceModule } from '../workspace/workspace.module'; AdminWorkspacesComponent, LockingStatusComponent, ActiveRunsComponent, + VlansComponent, + PoolListComponent, + PartitionListComponent, + PartitionComponent, + VlanListComponent, + PoolsComponent, + ProjectVlansComponent, + PoolListItemComponent, ], imports: [ ClipboardModule, CommonModule, - ProjectModule, + SharedModule, + RouterModule, FlexLayoutModule, FormsModule, + CwdTableModule, + WorkspaceModule, + ScrollingModule, + TableVirtualScrollModule, + AdminAppRoutingModule, MatButtonModule, MatCardModule, MatCheckboxModule, @@ -66,19 +95,15 @@ import { WorkspaceModule } from '../workspace/workspace.module'; MatProgressBarModule, MatProgressSpinnerModule, MatSidenavModule, + MatSlideToggleModule, MatSortModule, MatTooltipModule, MatTreeModule, - RouterModule, - MatSlideToggleModule, - CwdTableModule, - WorkspaceModule, - ], - exports: [ - AdminContainerComponent, - MatPaginatorModule, - UsersComponent, - UserListComponent, + MatSelectModule, + MatTabsModule, + MatSliderModule, + MatTableModule, ], + exports: [AdminContainerComponent, UsersComponent, UserListComponent], }) export class AdminAppModule {} diff --git a/src/app/admin-app/component/admin-container/admin-container.component.html b/src/app/admin-app/component/admin-container/admin-container.component.html index 719589b9..21e76dbc 100644 --- a/src/app/admin-app/component/admin-container/admin-container.component.html +++ b/src/app/admin-app/component/admin-container/admin-container.component.html @@ -64,6 +64,21 @@

Administration

{{ workspacesText }}
+
+ +
+ + + +
+
+ +
+ +
{{ vlansText }}
+
+
+
+ + + +
+ +
+
+
+
diff --git a/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.scss b/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.scss new file mode 100644 index 00000000..d241d618 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.scss @@ -0,0 +1,18 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.content { + display: flex; + justify-content: center; +} + +.accordion { + width: 75%; +} + +.header { + display: flex; + align-items: center; +} diff --git a/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.spec.ts b/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.spec.ts new file mode 100644 index 00000000..caf0c116 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PartitionListComponent } from './partition-list.component'; + +describe('PartitionListComponent', () => { + let component: PartitionListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PartitionListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PartitionListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.ts b/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.ts new file mode 100644 index 00000000..56093454 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition-list/partition-list.component.ts @@ -0,0 +1,56 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { + Component, + OnInit, + ChangeDetectionStrategy, + Input, +} from '@angular/core'; +import { Observable } from 'rxjs'; +import { Partition } from 'src/app/generated/caster-api'; +import { PartitionQuery } from 'src/app/vlans/state/partition/partition.query'; +import { PartitionService } from 'src/app/vlans/state/partition/partition.service'; +import { VlanService } from 'src/app/vlans/state/vlan/vlan.service'; + +@Component({ + selector: 'cas-partition-list', + templateUrl: './partition-list.component.html', + styleUrls: ['./partition-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PartitionListComponent implements OnInit { + @Input() + set poolId(id: string) { + this._poolId = id; + this.loadPool(id); + } + + get poolId() { + return this._poolId; + } + + _poolId: string; + + partitions$: Observable; + + constructor( + private vlanService: VlanService, + private partitionService: PartitionService, + private partitionQuery: PartitionQuery + ) {} + + ngOnInit(): void {} + + loadPool(poolId: string) { + this.vlanService.loadByPoolId(poolId).subscribe(); + this.partitionService.loadByPoolId(poolId).subscribe(); + this.partitions$ = this.partitionQuery.selectByPoolId(poolId); + } + + createPartition() { + this.partitionService.create(this.poolId).subscribe(); + } +} diff --git a/src/app/admin-app/component/admin-vlans/partition/partition.component.html b/src/app/admin-app/component/admin-vlans/partition/partition.component.html new file mode 100644 index 00000000..d35c8a1f --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition/partition.component.html @@ -0,0 +1,134 @@ + + + + + + + + + +

{{ partition?.name }}

+ +
+ + + + + + + +
+

Unassigned

+
+ +
{{ vlans.length }} VLANs
+
+
+ + + + + + +
+
+ + + +
diff --git a/src/app/admin-app/component/admin-vlans/partition/partition.component.scss b/src/app/admin-app/component/admin-vlans/partition/partition.component.scss new file mode 100644 index 00000000..c178dd62 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition/partition.component.scss @@ -0,0 +1,18 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.header-description { + justify-content: right; + align-items: center; +} + +.vlan-amount { + width: 6ch; + text-align: center; +} + +.title { + align-items: center; +} diff --git a/src/app/admin-app/component/admin-vlans/partition/partition.component.spec.ts b/src/app/admin-app/component/admin-vlans/partition/partition.component.spec.ts new file mode 100644 index 00000000..3f4ce854 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition/partition.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PartitionComponent } from './partition.component'; + +describe('PartitionComponent', () => { + let component: PartitionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PartitionComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PartitionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/partition/partition.component.ts b/src/app/admin-app/component/admin-vlans/partition/partition.component.ts new file mode 100644 index 00000000..6864471c --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/partition/partition.component.ts @@ -0,0 +1,113 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { + Component, + OnInit, + ChangeDetectionStrategy, + Input, +} from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Observable } from 'rxjs'; +import { Partition, Vlan } from 'src/app/generated/caster-api'; +import { ConfirmDialogService } from 'src/app/sei-cwd-common/confirm-dialog/service/confirm-dialog.service'; +import { PartitionQuery } from 'src/app/vlans/state/partition/partition.query'; +import { PartitionService } from 'src/app/vlans/state/partition/partition.service'; +import { VlanQuery } from 'src/app/vlans/state/vlan/vlan.query'; +import { VlanService } from 'src/app/vlans/state/vlan/vlan.service'; + +@Component({ + selector: 'cas-partition', + templateUrl: './partition.component.html', + styleUrls: ['./partition.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PartitionComponent implements OnInit { + @Input() set poolId(poolId: string) { + this.vlans$ = this.vlanQuery.selectUnassignedByPoolId(poolId); + this.otherPartitions$ = this.partitionQuery.selectByPoolId( + poolId, + this.partition?.id + ); + } + + @Input() set partition(partition: Partition) { + this._partition = partition; + this.vlans$ = this.vlanQuery.selectByPartitionId(partition.id); + this.otherPartitions$ = this.partitionQuery.selectByPoolId( + partition.poolId, + partition.id + ); + } + + get partition() { + return this._partition; + } + + _partition: Partition; + + editing: boolean; + + vlans$: Observable; + otherPartitions$: Observable; + + constructor( + private partitionService: PartitionService, + private partitionQuery: PartitionQuery, + private vlanService: VlanService, + private vlanQuery: VlanQuery, + private confirmService: ConfirmDialogService, + private snackBar: MatSnackBar + ) {} + + ngOnInit(): void {} + + delete($event) { + $event.stopPropagation(); + + this.confirmService + .confirmDialog( + 'Delete Partition', + `Are you sure you want to delete ${this.partition.name}?` + ) + .subscribe((x) => { + if (!x.wasCancelled) { + this.partitionService.delete(this.partition.id).subscribe(); + } + }); + } + + rename($event, target) { + $event.stopPropagation(); + + this.partitionService + .partialEdit(this.partition.id, { name: target.value }) + .subscribe(() => (this.editing = false)); + } + + addVlans($event, vlans) { + $event.stopPropagation(); + this.vlanService.addToPartition(this.partition.id, vlans).subscribe(); + } + + removeVlans($event, vlans) { + $event.stopPropagation(); + this.vlanService.removeFromPartition(this.partition.id, vlans).subscribe(); + } + + unsetDefault($event, id: string) { + $event.stopPropagation(); + this.partitionService.unsetDefault(id).subscribe(); + } + + setDefault($event, id: string) { + $event.stopPropagation(); + this.partitionService.setDefault(id).subscribe(); + } + + onClipboardSuccess() { + this.snackBar.open('Copied to clipboard', 'Dismiss'); + } +} diff --git a/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.html b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.html new file mode 100644 index 00000000..8985f40c --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.html @@ -0,0 +1,75 @@ + + + + +

{{ pool?.name }}

+ + + + + +
+ + + + + + + +
+ +
+
+
diff --git a/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.scss b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.scss new file mode 100644 index 00000000..5f2e155d --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.scss @@ -0,0 +1,22 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.flex { + display: flex; +} + +.align-right { + margin-left: auto; +} + +.header { + margin: 0; + padding: 0; + border: 0; +} + +.icon { + height: auto; +} diff --git a/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.spec.ts b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.spec.ts new file mode 100644 index 00000000..3be1e6c9 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PoolListItemComponent } from './pool-list-item.component'; + +describe('PoolListItemComponent', () => { + let component: PoolListItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PoolListItemComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.ts b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.ts new file mode 100644 index 00000000..a4402133 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list-item/pool-list-item.component.ts @@ -0,0 +1,79 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { + Component, + OnInit, + ChangeDetectionStrategy, + Input, + EventEmitter, + Output, + ViewChild, +} from '@angular/core'; +import { Pool } from 'src/app/generated/caster-api'; +import { ConfirmDialogService } from 'src/app/sei-cwd-common/confirm-dialog/service/confirm-dialog.service'; +import { PoolService } from 'src/app/vlans/state/pool/pool.service'; + +@Component({ + selector: 'cas-pool-list-item', + templateUrl: './pool-list-item.component.html', + styleUrls: ['./pool-list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PoolListItemComponent implements OnInit { + @Input() pool: Pool; + + @Output() poolSelected = new EventEmitter(); + + @ViewChild('nameInput') nameInput: Input; + + editing: boolean; + + constructor( + private confirmService: ConfirmDialogService, + private poolService: PoolService + ) {} + + ngOnInit(): void {} + + rename(target) { + this.poolService + .partialEdit(this.pool.id, { name: target.value }) + .subscribe(() => (this.editing = false)); + } + + deletePool(pool: Pool) { + this.confirmService + .confirmDialog( + 'Delete Pool', + `Are you sure you want to delete ${pool.name}?` + ) + .subscribe((x) => { + if (!x.wasCancelled) { + this.poolService.delete(pool.id, false).subscribe( + () => {}, + (error) => { + if (error?.status == 409) { + this.confirmService + .confirmDialog( + 'Delete Failed', + `${pool.name} has VLANs that are in use. Do you want to force delete it?` + ) + .subscribe((x) => { + if (!x.wasCancelled) { + this.poolService.delete(pool.id, true).subscribe(); + } + }); + } + } + ); + } + }); + } + + selectPool(pool: Pool) { + this.poolSelected.emit(pool); + } +} diff --git a/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.html b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.html new file mode 100644 index 00000000..80acebff --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.html @@ -0,0 +1,42 @@ + + +
+

Pools

+ + +
+
+

+ Pools are groups of 4096 VLANs, each representing a standard switching + infrastructure. Each Pool can be sub-divided into Partitions of various + sizes. Partitions can be assigned to Projects and when a VLAN is requested + in a Workspace, it will be pulled from the available VLANs in it's Project's + assigned Partition. A system-wide Default Partition can be selected, which + will cause VLANs to be allocated from that Partition if no Project or + Partition are specified in a request. +

+
+
+ +
diff --git a/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.scss b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.scss new file mode 100644 index 00000000..f15e9af1 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.scss @@ -0,0 +1,15 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 5% 1%; +} + +.header { + display: flex; + align-items: center; +} diff --git a/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.spec.ts b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.spec.ts new file mode 100644 index 00000000..13149b2c --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PoolListComponent } from './pool-list.component'; + +describe('PoolListComponent', () => { + let component: PoolListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PoolListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.ts b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.ts new file mode 100644 index 00000000..e804aa7c --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pool-list/pool-list.component.ts @@ -0,0 +1,45 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { + Component, + OnInit, + ChangeDetectionStrategy, + Input, + Output, + EventEmitter, +} from '@angular/core'; +import { Pool } from 'src/app/generated/caster-api'; +import { PoolService } from 'src/app/vlans/state/pool/pool.service'; + +@Component({ + selector: 'cas-pool-list', + templateUrl: './pool-list.component.html', + styleUrls: ['./pool-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PoolListComponent implements OnInit { + @Input() pools: Pool[]; + + @Output() poolSelected = new EventEmitter(); + + showDocumentation = false; + + constructor(private poolService: PoolService) {} + + ngOnInit(): void {} + + createPool() { + this.poolService.create().subscribe(); + } + + selectPool(pool: Pool) { + this.poolSelected.emit(pool); + } + + toggleDocumentation() { + this.showDocumentation = !this.showDocumentation; + } +} diff --git a/src/app/admin-app/component/admin-vlans/pools/pools.component.html b/src/app/admin-app/component/admin-vlans/pools/pools.component.html new file mode 100644 index 00000000..ae0847bc --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pools/pools.component.html @@ -0,0 +1,21 @@ + + + + +
+
+ +

Pool - {{ selectedPool.name }}

+
+ + +
diff --git a/src/app/admin-app/component/admin-vlans/pools/pools.component.scss b/src/app/admin-app/component/admin-vlans/pools/pools.component.scss new file mode 100644 index 00000000..d318214d --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pools/pools.component.scss @@ -0,0 +1,10 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.header { + display: flex; + align-items: center; + gap: 2px; +} diff --git a/src/app/admin-app/component/admin-vlans/pools/pools.component.spec.ts b/src/app/admin-app/component/admin-vlans/pools/pools.component.spec.ts new file mode 100644 index 00000000..9049ba57 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pools/pools.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PoolsComponent } from './pools.component'; + +describe('PoolsComponent', () => { + let component: PoolsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PoolsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/pools/pools.component.ts b/src/app/admin-app/component/admin-vlans/pools/pools.component.ts new file mode 100644 index 00000000..9e4a87fb --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/pools/pools.component.ts @@ -0,0 +1,39 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Pool } from 'src/app/generated/caster-api'; +import { PoolQuery } from 'src/app/vlans/state/pool/pool.query'; +import { PoolService } from 'src/app/vlans/state/pool/pool.service'; + +@Component({ + selector: 'cas-pools', + templateUrl: './pools.component.html', + styleUrls: ['./pools.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PoolsComponent implements OnInit { + public pools$ = this.poolQuery.selectAll(); + + public showPools = true; + public selectedPool$: Observable; + + constructor(private poolService: PoolService, private poolQuery: PoolQuery) {} + + ngOnInit(): void { + this.poolService.load().subscribe(); + } + + selectPool(pool: Pool) { + this.showPools = false; + this.selectedPool$ = this.poolQuery.selectEntity(pool.id); + } + + deselectPool() { + this.showPools = true; + this.selectedPool$ = of(null); + } +} diff --git a/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.html b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.html new file mode 100644 index 00000000..c76a0370 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.html @@ -0,0 +1,57 @@ + + +

+ Assign a Partition to each Project. Requested VLANs for that Project will be + taken from the assigned Partition, or the system-wide Default Partition. +

+
+ + + + + + + + + + + + + +
Name{{ element.name }}Partition + + Partition + + -- None -- + + + {{ partition.name }} + + + + + + + +
+
diff --git a/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.scss b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.scss new file mode 100644 index 00000000..7414af63 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.scss @@ -0,0 +1,10 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.center { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.spec.ts b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.spec.ts new file mode 100644 index 00000000..04f5509b --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectVlansComponent } from './project-vlans.component'; + +describe('ProjectVlansComponent', () => { + let component: ProjectVlansComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProjectVlansComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectVlansComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.ts b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.ts new file mode 100644 index 00000000..98751573 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/project-vlans/project-vlans.component.ts @@ -0,0 +1,105 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { + Component, + OnInit, + ChangeDetectionStrategy, + Input, +} from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { + BehaviorSubject, + combineLatest, + forkJoin, + Observable, + timer, +} from 'rxjs'; +import { finalize, map, take } from 'rxjs/operators'; +import { Partition } from 'src/app/generated/caster-api'; +import { Pool } from 'src/app/generated/caster-api'; +import { Project } from 'src/app/generated/caster-api'; +import { ProjectService } from 'src/app/project'; +import { PartitionQuery } from 'src/app/vlans/state/partition/partition.query'; +import { PartitionService } from 'src/app/vlans/state/partition/partition.service'; +import { PoolQuery } from 'src/app/vlans/state/pool/pool.query'; +import { PoolService } from 'src/app/vlans/state/pool/pool.service'; + +@Component({ + selector: 'cas-project-vlans', + templateUrl: './project-vlans.component.html', + styleUrls: ['./project-vlans.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProjectVlansComponent implements OnInit { + @Input() set projects(projects: Project[]) { + this.dataSource.data = projects; + } + + dataSource = new MatTableDataSource(); + partitionOptions$: Observable>; + + // #region loading + private loadingSubject = new BehaviorSubject>( + new Map() + ); + public loading$ = this.loadingSubject.asObservable(); + + private setLoading(projectId: string, val: boolean) { + this.loadingSubject.next( + this.loadingSubject.getValue().set(projectId, val) + ); + } + // #endregion + + displayedColumns: string[] = ['name', 'partition']; + + constructor( + private projectService: ProjectService, + private poolService: PoolService, + private partitionService: PartitionService, + private poolQuery: PoolQuery, + private partitionQuery: PartitionQuery + ) {} + + ngOnInit(): void { + forkJoin([ + this.projectService.loadProjects(), + this.poolService.load(), + this.partitionService.load(), + ]).subscribe(); + + this.partitionOptions$ = combineLatest([ + this.poolQuery.selectAll(), + this.partitionQuery.selectAll(), + ]).pipe( + map(([pools, partitions]) => { + const map = new Map(); + + pools.forEach((x) => { + let part = partitions.filter((y) => y.poolId == x.id); + map.set(x, part); + }); + + return map; + }) + ); + } + + updatePartition(projectId: string, partitionId: string) { + this.setLoading(projectId, true); + + this.projectService + .assignPartition(projectId, partitionId) + .pipe( + take(1), + finalize(() => { + // make sure progress bar is shown + timer(500).subscribe(() => this.setLoading(projectId, false)); + }) + ) + .subscribe(); + } +} diff --git a/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.html b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.html new file mode 100644 index 00000000..32a551d4 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.html @@ -0,0 +1,189 @@ + + +
+ + Search + + + +
+ + Partition + + + Unassigned + + + {{ partition.name }} + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Id{{ element.vlanId }}In Use + + Reserved + + Tag +
+ +

{{ element.tag }}

+ +
+ + + + + + + +
Actions + +
+ No data matching the filter "{{ input.value }}" +
+
diff --git a/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.scss b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.scss new file mode 100644 index 00000000..bcf54d69 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.scss @@ -0,0 +1,22 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.full-width { + width: 100%; +} + +.header-row { + display: flex; +} + +.right { + margin-left: auto; +} + +.tag { + display: flex; + align-items: center; + width: fit-content; +} diff --git a/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.spec.ts b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.spec.ts new file mode 100644 index 00000000..e764a6a0 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VlanListComponent } from './vlan-list.component'; + +describe('VlanListComponent', () => { + let component: VlanListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ VlanListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VlanListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.ts b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.ts new file mode 100644 index 00000000..41bb85cb --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlan-list/vlan-list.component.ts @@ -0,0 +1,211 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { SelectionModel } from '@angular/cdk/collections'; +import { + Component, + OnInit, + ChangeDetectionStrategy, + Input, + ViewChild, + ChangeDetectorRef, +} from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Observable } from 'rxjs'; +import { finalize, take } from 'rxjs/operators'; +import { ConfirmDialogService } from 'src/app/sei-cwd-common/confirm-dialog/service/confirm-dialog.service'; +import { VlanService } from 'src/app/vlans/state/vlan/vlan.service'; +import { TableVirtualScrollDataSource } from 'ng-table-virtual-scroll'; +import { Partition, Vlan } from 'src/app/generated/caster-api'; + +@Component({ + selector: 'cas-vlan-list', + templateUrl: './vlan-list.component.html', + styleUrls: ['./vlan-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VlanListComponent implements OnInit { + @Input() showUnassigned = true; + @Input() partitions: Partition[]; + + @Input() set vlans(vlans: Vlan[]) { + this._vlans = vlans; + this.dataSource.data = vlans.sort((a, b) => a.vlanId - b.vlanId); + this.calculateTableHeight(); + + // deselect items if they no longer exist in the data + this.selection.selected.forEach((x) => { + if (!vlans.map((y) => y.id).includes(x.id)) { + this.selection.deselect(x); + } + }); + } + + get vlans() { + return this._vlans; + } + + _vlans: Vlan[]; + dataSource = new TableVirtualScrollDataSource(); + selection = new SelectionModel(true, []); + displayedColumns: string[] = [ + 'select', + 'vlanId', + 'inUse', + 'reserved', + 'tag', + 'actions', + ]; + loading = new Map(); + editingId: string; + + public itemSize = 48; + public headerSize = 56; + public maxSize = this.itemSize * 7; + public tableHeight = '0px'; + public readonly unassigned = 'UNASSIGNED'; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + constructor( + private changeDetector: ChangeDetectorRef, + private confirmService: ConfirmDialogService, + private vlanService: VlanService + ) {} + + ngOnInit(): void { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.dataSource.filterPredicate = function (data, filter: string): boolean { + return ( + data.vlanId.toString().toLowerCase().includes(filter) || + data.inUse.toString().toLowerCase().includes(filter) || + data.reserved.toString().toLowerCase().includes(filter) || + data.tag?.toLowerCase().includes(filter) + ); + }; + } + + applyFilter(event: Event) { + const filterValue = (event.target as HTMLInputElement).value; + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + toggleInUse(vlan: Vlan) { + this.confirmService + .confirmDialog( + 'Confirm VLAN', + `Are you sure you want to set VLAN ${vlan.vlanId} to ${ + vlan.inUse ? 'Not In Use' : 'In Use' + }?` + ) + .subscribe((x) => { + if (!x.wasCancelled) { + if (vlan.inUse) { + this.execute(vlan, this.vlanService.release(vlan)); + } else { + this.execute(vlan, this.vlanService.acquire(vlan)); + } + } + }); + } + + toggleReserved(vlan: Vlan) { + if (vlan.reserved) { + this.execute(vlan, this.vlanService.unreserve(vlan)); + } else { + this.execute(vlan, this.vlanService.reserve(vlan)); + } + } + + removeFromPartition(vlan: Vlan) { + this.confirmService + .confirmDialog( + 'Confirm Remove', + `Are you sure you want to remove VLAN ${vlan.vlanId} from this Partition?` + ) + .subscribe((x) => { + if (!x.wasCancelled) { + this.execute( + vlan, + this.vlanService.reassign( + [].concat(vlan.id), + vlan.partitionId, + null + ) + ); + } + }); + } + + reassignSelected(toPartitionId: string) { + this.confirmService + .confirmDialog( + 'Confirm Reassign', + `Are you sure you want to reassign ${this.selection.selected.length} selected VLANs?` + ) + .subscribe((x) => { + if (!x.wasCancelled) { + this.vlanService + .reassign( + this.selection.selected.map((x) => x.id), + this.selection.selected[0].partitionId, + toPartitionId == this.unassigned ? null : toPartitionId + ) + .subscribe(); + } + }); + } + + editTag(id: string, target) { + this.vlanService + .partialEdit(id, { tag: target.value }) + .subscribe(() => (this.editingId = null)); + } + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.dataSource.data.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + toggleAllRows() { + if (this.isAllSelected()) { + this.selection.clear(); + return; + } + + this.selection.select(...this.dataSource.data); + } + + calculateTableHeight() { + const count = this.dataSource.filteredData.length; + let height: number; + height = this.headerSize * 1.2 + count * this.itemSize; + + if (height > this.maxSize) { + height = this.maxSize; + } + + this.tableHeight = `${height}px`; + } + + private execute(vlan: Vlan, sub: Observable) { + this.loading.set(vlan.id, true); + sub + .pipe( + take(1), + finalize(() => { + this.loading.set(vlan.id, false); + this.changeDetector.markForCheck(); + }) + ) + .subscribe(); + } +} diff --git a/src/app/admin-app/component/admin-vlans/vlans.component.html b/src/app/admin-app/component/admin-vlans/vlans.component.html new file mode 100644 index 00000000..d6b34bc8 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlans.component.html @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/src/app/admin-app/component/admin-vlans/vlans.component.scss b/src/app/admin-app/component/admin-vlans/vlans.component.scss new file mode 100644 index 00000000..43adddb1 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlans.component.scss @@ -0,0 +1,9 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +.tab-content { + margin-bottom: 1%; + margin-right: 2%; +} diff --git a/src/app/admin-app/component/admin-vlans/vlans.component.spec.ts b/src/app/admin-app/component/admin-vlans/vlans.component.spec.ts new file mode 100644 index 00000000..b1cb7072 --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlans.component.spec.ts @@ -0,0 +1,30 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VlansComponent } from './vlans.component'; + +describe('VlansComponent', () => { + let component: VlansComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ VlansComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VlansComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-app/component/admin-vlans/vlans.component.ts b/src/app/admin-app/component/admin-vlans/vlans.component.ts new file mode 100644 index 00000000..335e00cd --- /dev/null +++ b/src/app/admin-app/component/admin-vlans/vlans.component.ts @@ -0,0 +1,43 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { + Component, + OnInit, + ChangeDetectionStrategy, + OnDestroy, +} from '@angular/core'; +import { ProjectQuery } from 'src/app/project'; +import { SignalRService } from 'src/app/shared/signalr/signalr.service'; + +@Component({ + selector: 'cas-vlans', + templateUrl: './vlans.component.html', + styleUrls: ['./vlans.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VlansComponent implements OnInit, OnDestroy { + public projects$ = this.projectQuery.selectAll(); + + constructor( + private projectQuery: ProjectQuery, + private signalRService: SignalRService + ) {} + + ngOnInit(): void { + this.signalRService + .startConnection() + .then(() => { + this.signalRService.joinVlansAdmin(); + }) + .catch((err) => { + console.log(err); + }); + } + + ngOnDestroy() { + this.signalRService.leaveVlansAdmin(); + } +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index d52139ed..761cf7c2 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -2,16 +2,16 @@ // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; import { ComnAuthGuardService } from '@cmusei/crucible-common'; -import { AdminContainerComponent } from './admin-app/component/admin-container/admin-container.component'; import { ProjectListContainerComponent } from './project/component/project-home/project-list-container/project-list-container.component'; const routes: Routes = [ { path: 'admin', - component: AdminContainerComponent, pathMatch: 'full', + loadChildren: () => + import('./admin-app/admin-app.module').then((m) => m.AdminAppModule), canActivate: [ComnAuthGuardService], }, { @@ -28,6 +28,7 @@ const routes: Routes = [ paramsInheritanceStrategy: 'always', onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy', + preloadingStrategy: PreloadAllModules, }), ], exports: [RouterModule], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 01d08007..76b95723 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -7,6 +7,7 @@ import { ErrorHandler, NgModule } from '@angular/core'; import { FlexLayoutModule } from '@angular/flex-layout'; import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; @@ -32,12 +33,12 @@ import { AkitaNgRouterStoreModule } from '@datorama/akita-ng-router-store'; import { AkitaNgDevtools } from '@datorama/akita-ngdevtools'; import { HotkeysModule } from '@ngneat/hotkeys'; import { environment } from '../environments/environment'; -import { AdminAppModule } from './admin-app/admin-app.module'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { DesignModule } from './designs/design.module'; import { ApiModule, BASE_PATH } from './generated/caster-api'; import { ProjectModule } from './project/project.module'; +import { CwdDialogsModule } from './sei-cwd-common/confirm-dialog/cwd-dialogs.module'; import { ErrorService } from './sei-cwd-common/cwd-error/error.service'; import { SystemMessageComponent } from './sei-cwd-common/cwd-system-message/components/system-message.component'; import { SystemMessageService } from './sei-cwd-common/cwd-system-message/services/system-message.service'; @@ -65,46 +66,47 @@ export const myCustomSnackBarDefaults: MatSnackBarConfig = { }; @NgModule({ - declarations: [AppComponent, SystemMessageComponent], - imports: [ - BrowserModule, - BrowserAnimationsModule, - environment.production ? [] : AkitaNgDevtools.forRoot(), - AkitaNgRouterStoreModule, - AppRoutingModule, - ComnSettingsModule.forRoot(), - ComnAuthModule.forRoot(), - AdminAppModule, - ApiModule, - CwdToolbarModule, - MatMenuModule, - MatButtonModule, - MatIconModule, - MatTooltipModule, - MatBottomSheetModule, - MatExpansionModule, - MatToolbarModule, - ProjectModule, - HttpClientModule, - FlexLayoutModule, - OverlayModule, - HotkeysModule, - DesignModule, - ], - providers: [ - { - provide: BASE_PATH, - useFactory: getBasePath, - deps: [ComnSettingsService], - }, - { provide: ErrorHandler, useClass: ErrorService }, - SystemMessageService, - { provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: myCustomTooltipDefaults }, - { - provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, - useValue: myCustomSnackBarDefaults, - }, - ], - bootstrap: [AppComponent] + declarations: [AppComponent, SystemMessageComponent], + imports: [ + BrowserModule, + BrowserAnimationsModule, + environment.production ? [] : AkitaNgDevtools.forRoot(), + AkitaNgRouterStoreModule, + AppRoutingModule, + ComnSettingsModule.forRoot(), + ComnAuthModule.forRoot(), + ApiModule, + CwdToolbarModule, + MatMenuModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + MatBottomSheetModule, + MatExpansionModule, + MatToolbarModule, + ProjectModule, + HttpClientModule, + FlexLayoutModule, + OverlayModule, + HotkeysModule, + DesignModule, + MatCheckboxModule, + CwdDialogsModule, + ], + providers: [ + { + provide: BASE_PATH, + useFactory: getBasePath, + deps: [ComnSettingsService], + }, + { provide: ErrorHandler, useClass: ErrorService }, + SystemMessageService, + { provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: myCustomTooltipDefaults }, + { + provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, + useValue: myCustomSnackBarDefaults, + }, + ], + bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/editor/component/editor/editor.component.html b/src/app/editor/component/editor/editor.component.html index c30f7b5f..4b9f5bdb 100644 --- a/src/app/editor/component/editor/editor.component.html +++ b/src/app/editor/component/editor/editor.component.html @@ -21,7 +21,7 @@ mwlResizeHandle [resizeEdges]="{ left: true }" > -
+
+
+ +
diff --git a/src/app/sei-cwd-common/confirm-dialog/cwd-dialogs.module.ts b/src/app/sei-cwd-common/confirm-dialog/cwd-dialogs.module.ts new file mode 100644 index 00000000..fe88ca90 --- /dev/null +++ b/src/app/sei-cwd-common/confirm-dialog/cwd-dialogs.module.ts @@ -0,0 +1,30 @@ +// Copyright 2021 Carnegie Mellon University. All Rights Reserved. +// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatDialogModule } from '@angular/material/dialog'; +import { ConfirmDialogComponent } from './components/confirm-dialog.component'; +import { ConfirmDialogService } from './service/confirm-dialog.service'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { FormsModule } from '@angular/forms'; +import { FlexLayoutModule } from '@angular/flex-layout'; + +@NgModule({ + declarations: [ConfirmDialogComponent], + exports: [], + imports: [ + CommonModule, + MatDialogModule, + MatCheckboxModule, + MatButtonModule, + MatTooltipModule, + FormsModule, + FlexLayoutModule, + ], + entryComponents: [ConfirmDialogComponent], + providers: [ConfirmDialogService], +}) +export class CwdDialogsModule {} diff --git a/src/app/sei-cwd-common/sei-cwd-common.module.ts b/src/app/sei-cwd-common/sei-cwd-common.module.ts index 9eac14e3..fcb6d04a 100644 --- a/src/app/sei-cwd-common/sei-cwd-common.module.ts +++ b/src/app/sei-cwd-common/sei-cwd-common.module.ts @@ -6,6 +6,7 @@ import { InjectionToken, NgModule } from '@angular/core'; import { FlexLayoutModule } from '@angular/flex-layout'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; @@ -15,7 +16,6 @@ import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { BrowserModule } from '@angular/platform-browser'; import { SharedModule } from '../shared/shared.module'; import { NameDialogComponent } from './name-dialog/name-dialog.component'; @@ -25,7 +25,6 @@ export const CWD_SETTINGS_TOKEN = new InjectionToken('CwdSettings'); declarations: [NameDialogComponent], imports: [ CommonModule, - BrowserModule, SharedModule, MatCardModule, MatProgressSpinnerModule, @@ -39,6 +38,7 @@ export const CWD_SETTINGS_TOKEN = new InjectionToken('CwdSettings'); MatTooltipModule, MatTabsModule, MatDialogModule, + MatCheckboxModule, ], exports: [NameDialogComponent], }) diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index af3d1d45..465884a4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -3,12 +3,28 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { RouterModule } from '@angular/router'; +import { TopbarComponent } from './components/top-bar/topbar.component'; import { CrucibleHotkeyDirective } from './directives/crucible-hotkey.directive'; @NgModule({ - declarations: [CrucibleHotkeyDirective], - imports: [CommonModule, BrowserModule], - exports: [CrucibleHotkeyDirective] + declarations: [CrucibleHotkeyDirective, TopbarComponent], + imports: [ + CommonModule, + MatToolbarModule, + MatMenuModule, + MatButtonModule, + MatSlideToggleModule, + MatIconModule, + FlexLayoutModule, + RouterModule, + ], + exports: [CrucibleHotkeyDirective, TopbarComponent], }) export class SharedModule {} diff --git a/src/app/shared/signalr/signalr.service.ts b/src/app/shared/signalr/signalr.service.ts index 06d91121..af813b80 100644 --- a/src/app/shared/signalr/signalr.service.ts +++ b/src/app/shared/signalr/signalr.service.ts @@ -14,10 +14,16 @@ import { DesignModule, Directory, ModelFile, + Partition, + Pool, Run, Variable, + Vlan, Workspace, } from 'src/app/generated/caster-api'; +import { PartitionService } from 'src/app/vlans/state/partition/partition.service'; +import { PoolService } from 'src/app/vlans/state/pool/pool.service'; +import { VlanService } from 'src/app/vlans/state/vlan/vlan.service'; import { WorkspaceService } from 'src/app/workspace/state'; import { ProjectService } from '../../project/state'; @@ -30,6 +36,7 @@ export class SignalRService { private workspaceIds: string[] = []; private designIds: string[] = []; private joinedWorkspacesAdmin = false; + private joinedVlansAdmin = false; private connectionPromise: Promise; constructor( @@ -41,7 +48,10 @@ export class SignalRService { private settingsService: ComnSettingsService, private designService: DesignService, private variableService: VariableService, - private designModuleService: DesignModuleService + private designModuleService: DesignModuleService, + private poolService: PoolService, + private partitionService: PartitionService, + private vlanService: VlanService ) {} public startConnection(): Promise { @@ -85,6 +95,10 @@ export class SignalRService { if (this.designIds) { this.designIds.forEach((x) => this.joinDesign(x)); } + + if (this.joinedVlansAdmin) { + this.joinVlansAdmin(); + } } public joinProject(projectId: string) { @@ -139,6 +153,19 @@ export class SignalRService { this.hubConnection.invoke('LeaveDesign', designId); } + public joinVlansAdmin() { + this.joinedVlansAdmin = true; + + if (this.hubConnection.state === signalR.HubConnectionState.Connected) { + this.hubConnection.invoke('JoinVlansAdmin'); + } + } + + public leaveVlansAdmin() { + this.joinedVlansAdmin = false; + this.hubConnection.invoke('LeaveVlansAdmin'); + } + public streamPlanOutput(planId: string) { return this.hubConnection.stream('GetPlanOutput', planId); } @@ -155,6 +182,9 @@ export class SignalRService { this.addDesignHandlers(); this.addVariableHandlers(); this.addDesignModuleHandlers(); + this.addPoolHandlers(); + this.addPartitionHandlers(); + this.addVlanHandlers(); } private addFileHandlers() { @@ -264,6 +294,66 @@ export class SignalRService { }); } + private addPoolHandlers() { + this.hubConnection.on('PoolCreated', (pool: Pool) => { + this.poolService.add(pool); + }); + + this.hubConnection.on( + 'PoolUpdated', + (pool: Pool, modifiedProperties: string[]) => { + this.poolService.update( + pool.id, + this.getModified(pool, modifiedProperties) + ); + } + ); + + this.hubConnection.on('PoolDeleted', (id: string) => { + this.poolService.remove(id); + }); + } + + private addPartitionHandlers() { + this.hubConnection.on('PartitionCreated', (partition: Partition) => { + this.partitionService.add(partition); + }); + + this.hubConnection.on( + 'PartitionUpdated', + (partition: Partition, modifiedProperties: string[]) => { + this.partitionService.update( + partition.id, + this.getModified(partition, modifiedProperties) + ); + } + ); + + this.hubConnection.on('PartitionDeleted', (id: string) => { + this.partitionService.remove(id); + }); + } + + private addVlanHandlers() { + this.hubConnection.on('VlanCreated', (vlan: Vlan) => { + this.vlanService.add(vlan); + }); + + this.hubConnection.on( + 'VlanUpdated', + (vlan: Vlan, modifiedProperties: string[]) => { + this.vlanService.update( + vlan.id, + this.getModified(vlan, modifiedProperties) + ); + } + ); + + this.hubConnection.on('VlanDeleted', (id: string) => { + this.vlanService.remove(id); + }); + } + private getModified(entity: any, modifiedProperties: string[]): any { if (modifiedProperties == null) { return entity; diff --git a/src/app/vlans/state/partition/partition.query.ts b/src/app/vlans/state/partition/partition.query.ts new file mode 100644 index 00000000..97598f97 --- /dev/null +++ b/src/app/vlans/state/partition/partition.query.ts @@ -0,0 +1,25 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { Order, QueryConfig, QueryEntity } from '@datorama/akita'; +import { PartitionStore, PartitionState } from './partition.store'; + +@Injectable({ providedIn: 'root' }) +@QueryConfig({ + sortBy: 'name', + sortByOrder: Order.ASC, +}) +export class PartitionQuery extends QueryEntity { + constructor(protected store: PartitionStore) { + super(store); + } + + selectByPoolId(id: string, excludePartitionId: string = null) { + return this.selectAll({ + filterBy: (x) => x.poolId == id && x.id != excludePartitionId, + }); + } +} diff --git a/src/app/vlans/state/partition/partition.service.ts b/src/app/vlans/state/partition/partition.service.ts new file mode 100644 index 00000000..486bb812 --- /dev/null +++ b/src/app/vlans/state/partition/partition.service.ts @@ -0,0 +1,100 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { tap } from 'rxjs/operators'; +import { + EditPartitionCommand, + PartialEditPartitionCommand, + Partition, + VlansService, +} from 'src/app/generated/caster-api'; +import { PartitionStore } from './partition.store'; + +@Injectable({ providedIn: 'root' }) +export class PartitionService { + constructor( + private partitionStore: PartitionStore, + private vlansService: VlansService + ) {} + + load() { + this.partitionStore.setLoading(true); + return this.vlansService.getPartitions().pipe( + tap((partitions: Partition[]) => { + this.partitionStore.set(partitions); + this.partitionStore.setLoading(false); + }) + ); + } + + loadByPoolId(id: string) { + this.partitionStore.setLoading(true); + return this.vlansService.getPartitionsByPool(id).pipe( + tap((partitions: Partition[]) => { + this.partitionStore.set(partitions); + this.partitionStore.setLoading(false); + }) + ); + } + + create(poolId: string) { + return this.vlansService + .createPartition(poolId, { name: 'New Partition' }) + .pipe( + tap((partition: Partition) => { + this.add(partition); + }) + ); + } + + delete(id: string) { + return this.vlansService.deletePartition(id).pipe( + tap(() => { + this.remove(id); + }) + ); + } + + edit(id: string, command: EditPartitionCommand) { + return this.vlansService.editPartition(id, command).pipe( + tap((partition: Partition) => { + this.update(id, partition); + }) + ); + } + + partialEdit(id: string, command: PartialEditPartitionCommand) { + return this.vlansService.partialEditPartition(id, command).pipe( + tap((partition: Partition) => { + this.update(id, partition); + }) + ); + } + + unsetDefault(id: string) { + return this.vlansService + .unsetDefaultPartition() + .pipe(tap(() => this.update(id, { isDefault: false }))); + } + + setDefault(id: string) { + return this.vlansService + .setDefaultPartition(id) + .pipe(tap(() => this.update(id, { isDefault: true }))); + } + + add(partition: Partition) { + this.partitionStore.upsert(partition.id, partition); + } + + update(id, partition: Partial) { + this.partitionStore.update(id, partition); + } + + remove(id: string) { + this.partitionStore.remove(id); + } +} diff --git a/src/app/vlans/state/partition/partition.store.ts b/src/app/vlans/state/partition/partition.store.ts new file mode 100644 index 00000000..e31e227d --- /dev/null +++ b/src/app/vlans/state/partition/partition.store.ts @@ -0,0 +1,18 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { Partition } from 'src/app/generated/caster-api'; + +export interface PartitionState extends EntityState {} + +@Injectable({ providedIn: 'root' }) +@StoreConfig({ name: 'partition' }) +export class PartitionStore extends EntityStore { + constructor() { + super(); + } +} diff --git a/src/app/vlans/state/pool/pool.query.ts b/src/app/vlans/state/pool/pool.query.ts new file mode 100644 index 00000000..e577df4b --- /dev/null +++ b/src/app/vlans/state/pool/pool.query.ts @@ -0,0 +1,19 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { Order, QueryConfig, QueryEntity } from '@datorama/akita'; +import { PoolStore, PoolState } from './pool.store'; + +@Injectable({ providedIn: 'root' }) +@QueryConfig({ + sortBy: 'name', + sortByOrder: Order.ASC, +}) +export class PoolQuery extends QueryEntity { + constructor(protected store: PoolStore) { + super(store); + } +} diff --git a/src/app/vlans/state/pool/pool.service.ts b/src/app/vlans/state/pool/pool.service.ts new file mode 100644 index 00000000..2e4e2a6e --- /dev/null +++ b/src/app/vlans/state/pool/pool.service.ts @@ -0,0 +1,77 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { + EditPoolCommand, + PartialEditPoolCommand, + Pool, + VlansService, +} from 'src/app/generated/caster-api'; +import { PoolStore } from './pool.store'; + +@Injectable({ providedIn: 'root' }) +export class PoolService { + constructor( + private poolStore: PoolStore, + private vlansService: VlansService + ) {} + + load(): Observable { + this.poolStore.setLoading(true); + return this.vlansService.getPools().pipe( + tap((pools: Pool[]) => { + this.poolStore.set(pools); + this.poolStore.setLoading(false); + }) + ); + } + + create() { + return this.vlansService.createPool({ name: 'New Pool' }).pipe( + tap((pool: Pool) => { + this.add(pool); + }) + ); + } + + edit(id: string, command: EditPoolCommand) { + return this.vlansService.editPool(id, command).pipe( + tap((pool: Pool) => { + this.update(id, pool); + }) + ); + } + + partialEdit(id: string, command: PartialEditPoolCommand) { + return this.vlansService.partialEditPool(id, command).pipe( + tap((pool: Pool) => { + this.update(id, pool); + }) + ); + } + + delete(id: string, force: boolean) { + return this.vlansService.deletePool(id, { force: force }).pipe( + tap(() => { + this.remove(id); + }) + ); + } + + add(pool: Pool) { + this.poolStore.add(pool); + } + + update(id, pool: Partial) { + this.poolStore.update(id, pool); + } + + remove(id: string) { + this.poolStore.remove(id); + } +} diff --git a/src/app/vlans/state/pool/pool.store.ts b/src/app/vlans/state/pool/pool.store.ts new file mode 100644 index 00000000..8b9b7039 --- /dev/null +++ b/src/app/vlans/state/pool/pool.store.ts @@ -0,0 +1,18 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { Pool } from 'src/app/generated/caster-api'; + +export interface PoolState extends EntityState {} + +@Injectable({ providedIn: 'root' }) +@StoreConfig({ name: 'pool' }) +export class PoolStore extends EntityStore { + constructor() { + super(); + } +} diff --git a/src/app/vlans/state/vlan/vlan.query.ts b/src/app/vlans/state/vlan/vlan.query.ts new file mode 100644 index 00000000..038da164 --- /dev/null +++ b/src/app/vlans/state/vlan/vlan.query.ts @@ -0,0 +1,29 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { QueryEntity } from '@datorama/akita'; +import { VlanStore, VlanState } from './vlan.store'; + +@Injectable({ providedIn: 'root' }) +export class VlanQuery extends QueryEntity { + constructor(protected store: VlanStore) { + super(store); + } + + selectByPoolId(id: string) { + return this.selectAll({ filterBy: (x) => x.poolId == id }); + } + + selectUnassignedByPoolId(id: string) { + return this.selectAll({ + filterBy: (x) => x.poolId == id && !x.partitionId, + }); + } + + selectByPartitionId(id: string) { + return this.selectAll({ filterBy: (x) => x.partitionId == id }); + } +} diff --git a/src/app/vlans/state/vlan/vlan.service.ts b/src/app/vlans/state/vlan/vlan.service.ts new file mode 100644 index 00000000..e1b4d22f --- /dev/null +++ b/src/app/vlans/state/vlan/vlan.service.ts @@ -0,0 +1,132 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { + PartialEditVlanCommand, + Vlan, + VlansService, +} from 'src/app/generated/caster-api'; +import { VlanStore } from './vlan.store'; + +@Injectable({ providedIn: 'root' }) +export class VlanService { + constructor( + private vlanStore: VlanStore, + private vlansService: VlansService + ) {} + + loadByPoolId(id: string) { + this.vlanStore.setLoading(true); + return this.vlansService.getVlansByPool(id).pipe( + tap((vlans: Vlan[]) => { + this.vlanStore.set(vlans); + this.vlanStore.setLoading(false); + }) + ); + } + + partialEdit(id: string, command: PartialEditVlanCommand) { + return this.vlansService.partialEditVlan(id, command).pipe( + tap((vlan: Vlan) => { + this.update(id, vlan); + }) + ); + } + + addToPartition(partitionId: string, vlans: number) { + return this.vlansService + .addVlansToPartition(partitionId, { vlans: vlans }) + .pipe( + tap((vlans: Vlan[]) => { + this.vlanStore.upsertMany(vlans); + }) + ); + } + + removeFromPartition(partitionId: string, vlans: number) { + return this.vlansService + .removeVlansFromPartition(partitionId, { vlans: vlans }) + .pipe( + tap((vlans: Vlan[]) => { + this.vlanStore.upsertMany(vlans); + }) + ); + } + + acquire(vlan: Vlan) { + return this.vlansService + .acquireVlan({ partitionId: vlan.partitionId, vlanId: vlan.vlanId }) + .pipe( + tap((vlan: Vlan) => { + this.update(vlan.id, vlan); + }) + ); + } + + release(vlan: Vlan) { + return this.vlansService.releaseVlan(vlan.id).pipe( + tap((vlan: Vlan) => { + this.update(vlan.id, vlan); + }) + ); + } + + reserve(vlan: Vlan) { + return this.vlansService.partialEditVlan(vlan.id, { reserved: true }).pipe( + tap((vlan: Vlan) => { + this.update(vlan.id, vlan); + }) + ); + } + + unreserve(vlan: Vlan) { + return this.vlansService.partialEditVlan(vlan.id, { reserved: false }).pipe( + tap((vlan: Vlan) => { + this.update(vlan.id, vlan); + }) + ); + } + + reassign(vlanIds: string[], fromPartitionId: string, toPartitionId: string) { + let obs: Observable; + + if (fromPartitionId == null) { + obs = this.vlansService.addVlansToPartition(toPartitionId, { + vlanIds: vlanIds, + }); + } else if (toPartitionId == null) { + obs = this.vlansService.removeVlansFromPartition(fromPartitionId, { + vlanIds: vlanIds, + }); + } else { + obs = this.vlansService.reassignVlans({ + fromPartitionId: fromPartitionId, + toPartitionId: toPartitionId, + vlanIds: vlanIds, + }); + } + + return obs.pipe( + tap((vlans: Vlan[]) => { + this.vlanStore.upsertMany(vlans); + }) + ); + } + + add(vlan: Vlan) { + this.vlanStore.add(vlan); + } + + update(id, vlan: Partial) { + this.vlanStore.update(id, vlan); + } + + remove(id: string) { + this.vlanStore.remove(id); + } +} diff --git a/src/app/vlans/state/vlan/vlan.store.ts b/src/app/vlans/state/vlan/vlan.store.ts new file mode 100644 index 00000000..a35c7af1 --- /dev/null +++ b/src/app/vlans/state/vlan/vlan.store.ts @@ -0,0 +1,18 @@ +/* +Copyright 2021 Carnegie Mellon University. All Rights Reserved. + Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +*/ + +import { Injectable } from '@angular/core'; +import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; +import { Vlan } from 'src/app/generated/caster-api'; + +export interface VlanState extends EntityState {} + +@Injectable({ providedIn: 'root' }) +@StoreConfig({ name: 'vlan' }) +export class VlanStore extends EntityStore { + constructor() { + super(); + } +}