diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index b1a9bb0d..717e6ef9 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -69,6 +69,9 @@ import { GroupResolver } from "./resolvers/group.resolver"; import { GroupRoleResolver } from "./resolvers/group-role.resolver"; import { RoleDetailComponent } from "./components/role-management/role-detail/role-detail.component"; import { RoleDetailResolver } from "./resolvers/role-detail.resolver"; +import {ResourcePoolsManagementComponent} from "@components/resource-pools-management/resource-pools-management.component"; +import {ResourcePoolDetailsComponent} from "@components/resource-pool-details/resource-pool-details.component"; +import {ResourcePoolsResolver} from "@resolvers/resource-pools.resolver"; const routes: Routes = [ { @@ -99,6 +102,15 @@ const routes: Routes = [ groups: UserGroupsResolver, controller: ControllerResolve}, }, + { + path: 'controller/:controller_id/management/resourcePools/:pool_id', + component: ResourcePoolDetailsComponent, + canActivate: [LoginGuard], + resolve: { + pool: ResourcePoolsResolver, + controller: ControllerResolve + } + }, { path: 'installed-software', component: InstalledSoftwareComponent }, { path: 'controller/:controller_id/systemstatus', component: SystemStatusComponent, canActivate: [LoginGuard] }, @@ -231,6 +243,10 @@ const routes: Routes = [ { path: 'roles', component: RoleManagementComponent + }, + { + path: "resourcePools", + component: ResourcePoolsManagementComponent } ] }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b83802a7..59d0ff00 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -311,6 +311,12 @@ import { ConfirmationDeleteAllProjectsComponent } from './components/projects/co import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component'; import { PrivilegeComponent } from './components/role-management/role-detail/privilege/privilege.component'; import { GroupPrivilegesPipe } from './components/role-management/role-detail/privilege/group-privileges.pipe'; +import { ResourcePoolsManagementComponent } from './components/resource-pools-management/resource-pools-management.component'; +import { AddResourcePoolDialogComponent } from './components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component'; +import { DeleteResourcePoolComponent } from './components/resource-pools-management/delete-resource-pool/delete-resource-pool.component'; +import { ResourcePoolsFilterPipe } from './components/resource-pools-management/resource-pools-filter.pipe'; +import { ResourcePoolDetailsComponent } from './components/resource-pool-details/resource-pool-details.component'; +import { DeleteResourceConfirmationDialogComponent } from './components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component'; @NgModule({ declarations: [ @@ -535,6 +541,12 @@ import { GroupPrivilegesPipe } from './components/role-management/role-detail/pr ProjectMapLockConfirmationDialogComponent, PrivilegeComponent, GroupPrivilegesPipe, + ResourcePoolsManagementComponent, + AddResourcePoolDialogComponent, + DeleteResourcePoolComponent, + ResourcePoolsFilterPipe, + ResourcePoolDetailsComponent, + DeleteResourceConfirmationDialogComponent, ], imports: [ BrowserModule, diff --git a/src/app/components/management/management.component.ts b/src/app/components/management/management.component.ts index 4ec7311c..7dbed81c 100644 --- a/src/app/components/management/management.component.ts +++ b/src/app/components/management/management.component.ts @@ -23,7 +23,7 @@ import {ControllerService} from "@services/controller.service"; export class ManagementComponent implements OnInit { controller: Controller; - links = ['users', 'groups', 'roles']; + links = ['users', 'groups', 'roles', 'resourcePools']; activeLink: string = this.links[0]; constructor( diff --git a/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.html b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.html new file mode 100644 index 00000000..3fb7aab9 --- /dev/null +++ b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.html @@ -0,0 +1,5 @@ +
delete resource {{data.resource_type}}/{{data.name}} ?
+
+ + +
diff --git a/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.scss b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.scss new file mode 100644 index 00000000..6d80719b --- /dev/null +++ b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.scss @@ -0,0 +1,15 @@ +:host { + display: flex; + margin: 30px; + flex-direction: column; +} + +.title { + margin-bottom: 50px; +} + +.button { + display: flex; + flex-direction: row; + justify-content: space-between; +} diff --git a/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.spec.ts b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.spec.ts new file mode 100644 index 00000000..9a334b73 --- /dev/null +++ b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeleteResourceConfirmationDialogComponent } from './delete-resource-confirmation-dialog.component'; + +describe('DeleteResourceConfirmationDialogComponent', () => { + let component: DeleteResourceConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DeleteResourceConfirmationDialogComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DeleteResourceConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.ts b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.ts new file mode 100644 index 00000000..8ebeed13 --- /dev/null +++ b/src/app/components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component.ts @@ -0,0 +1,19 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {DIALOG_DATA} from "@angular/cdk/dialog"; +import {Resource} from "@models/resourcePools/Resource"; +import {MatDialogRef} from "@angular/material/dialog"; + +@Component({ + selector: 'app-delete-resource-confirmation-dialog', + templateUrl: './delete-resource-confirmation-dialog.component.html', + styleUrls: ['./delete-resource-confirmation-dialog.component.scss'] +}) +export class DeleteResourceConfirmationDialogComponent implements OnInit { + + constructor(@Inject(DIALOG_DATA) public data: Resource, + public dialogRef: MatDialogRef,) { } + + ngOnInit(): void { + } + +} diff --git a/src/app/components/resource-pool-details/resource-pool-details.component.html b/src/app/components/resource-pool-details/resource-pool-details.component.html new file mode 100644 index 00000000..bc2a77cc --- /dev/null +++ b/src/app/components/resource-pool-details/resource-pool-details.component.html @@ -0,0 +1,62 @@ +
+
+
+ + keyboard_arrow_left + +

role {{pool.name}} details

+
+
+
+
+ + pool name: + + +
+
creation date: {{pool.created_at}}
+
last update date: {{pool.updated_at}}
+
uuid: {{pool.resource_pool_id}}
+
+ +
+
+ +
+
+
+ + + + {{option}} + + +
+
+ +
+
+
+
{{resource.name}}
+
+ +
+
+
+
+
+
diff --git a/src/app/components/resource-pool-details/resource-pool-details.component.scss b/src/app/components/resource-pool-details/resource-pool-details.component.scss new file mode 100644 index 00000000..309df1a2 --- /dev/null +++ b/src/app/components/resource-pool-details/resource-pool-details.component.scss @@ -0,0 +1,67 @@ +.main { + display: flex; + justify-content: space-around; +} + +.details { + width: 30vw; + display: flex; + flex-direction: column; + justify-content: center; +} + + +.clickable { + cursor: pointer; +} + +.privilege { + display: flex; + flex-direction: row; + padding-left: 10px; + justify-content: space-between; +} + +.permission { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 10px; + border: 1px solid; + padding: 5px; + border-radius: 5px; + font-family: monospace; +} + +.header { + display: flex; + flex-direction: row; + justify-content: space-between; + padding-bottom: 20px; + padding-left: 10px; + +} +.header > div { + font-size: 2em; +} + +.resources { + display: flex; + flex-direction: column; +} + +.ownedResources { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + +} + +.addResource { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} diff --git a/src/app/components/resource-pool-details/resource-pool-details.component.spec.ts b/src/app/components/resource-pool-details/resource-pool-details.component.spec.ts new file mode 100644 index 00000000..f050718d --- /dev/null +++ b/src/app/components/resource-pool-details/resource-pool-details.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResourcePoolDetailsComponent } from './resource-pool-details.component'; + +describe('ResourcePoolDetailsComponent', () => { + let component: ResourcePoolDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ResourcePoolDetailsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ResourcePoolDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/resource-pool-details/resource-pool-details.component.ts b/src/app/components/resource-pool-details/resource-pool-details.component.ts new file mode 100644 index 00000000..82f3385e --- /dev/null +++ b/src/app/components/resource-pool-details/resource-pool-details.component.ts @@ -0,0 +1,122 @@ +import {Component, OnInit} from '@angular/core'; +import {Controller} from "@models/controller"; +import {FormControl, UntypedFormControl, UntypedFormGroup} from "@angular/forms"; +import {ToasterService} from "@services/toaster.service"; +import {ActivatedRoute} from "@angular/router"; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; +import {ResourcePoolsService} from "@services/resource-pools.service"; +import {ProjectService} from "@services/project.service"; +import {filter, map, startWith, switchMap} from "rxjs/operators"; +import {Project} from "@models/project"; +import {BehaviorSubject, Observable, of} from "rxjs"; +import {Resource} from "@models/resourcePools/Resource"; +import {MatDialog} from "@angular/material/dialog"; +import { + DeleteResourceConfirmationDialogComponent +} from "@components/resource-pool-details/delete-resource-confirmation-dialog/delete-resource-confirmation-dialog.component"; + +@Component({ + selector: 'app-resource-pool-details', + templateUrl: './resource-pool-details.component.html', + styleUrls: ['./resource-pool-details.component.scss'] +}) +export class ResourcePoolDetailsComponent implements OnInit { + + controller: Controller; + editPoolForm: UntypedFormGroup; + pool: ResourcePool; + addResourceFormControl = new FormControl(''); + addResourceFilteredOptions: Observable; + projects: Project[] = []; + + constructor(private toastService: ToasterService, + private route: ActivatedRoute, + private resourcePoolsService: ResourcePoolsService, + private dialog: MatDialog, + ) { + + + this.editPoolForm = new UntypedFormGroup({ + poolname: new UntypedFormControl(), + }); + } + + ngOnInit(): void { + this.route.data.subscribe((d: { controller: Controller; pool: ResourcePool }) => { + this.controller = d.controller; + this.pool = d.pool; + + this.refresh(); + + }); + + + } + + onUpdate() { + this.resourcePoolsService.update(this.controller, this.pool) + .subscribe((pool: ResourcePool) => { + this.toastService.success(`pool ${pool.name}, updated`); + }); + } + addResource() { + const selected = this.addResourceFormControl.value; + const project = this.projects.filter( p => p.name.includes(selected)); + if(project.length === 1) { + this.resourcePoolsService.addResource(this.controller,this.pool, project[0]) + .subscribe(() => { + this.toastService.success(`project : ${project[0].name}, added to pool: ${this.pool.name}`); + this.refresh(); + this.addResourceFormControl.setValue(''); + return; + }); + return; + } + if(project.length === 0) { + this.toastService.error(`cannot found related project with string: ${selected}`); + return; + } + if(project.length > 1) { + this.toastService.error(`${project.length} match ${selected}, please be more accurate`); + return; + } + } + + deleteResource(resource: Resource) { + this.dialog.open(DeleteResourceConfirmationDialogComponent, {data: resource}) + .afterClosed() + .subscribe((resource: Resource) => { + if(resource) { + this.resourcePoolsService + .deleteResource(this.controller, resource, this.pool) + .subscribe(() => { + this.refresh() + this.toastService.success(`resource ${resource.name} delete from pool ${this.pool.name}`) + }); + } + }) + } + + private refresh() { + this.resourcePoolsService + .get(this.controller, this.pool.resource_pool_id) + .subscribe((pool) => { + this.pool = pool; + }); + + this.resourcePoolsService + .getFreeResources(this.controller) + .subscribe((projects: Project[]) => { + this.projects = projects; + + this.addResourceFilteredOptions = this.addResourceFormControl.valueChanges.pipe( + startWith(''), + map((value: string) => { + return this.projects + .filter((project: Project) => project.name.toLowerCase().includes(value.toLowerCase() || '')) + .map((project: Project) => project.name); + }) + ); + }); + } +} diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/PoolNameAsyncValidator.ts b/src/app/components/resource-pools-management/add-resource-pool-dialog/PoolNameAsyncValidator.ts new file mode 100644 index 00000000..94eef5d7 --- /dev/null +++ b/src/app/components/resource-pools-management/add-resource-pool-dialog/PoolNameAsyncValidator.ts @@ -0,0 +1,29 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { UntypedFormControl } from '@angular/forms'; +import { timer } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { Controller } from "@models/controller"; +import {ResourcePoolsService} from "@services/resource-pools.service"; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; + +export const poolNameAsyncValidator = (controller: Controller, resourcePoolsService: ResourcePoolsService) => { + return (control: UntypedFormControl) => { + return timer(500).pipe( + switchMap(() => resourcePoolsService.getAll(controller)), + map((response: ResourcePool[]) => { + return (response.find((n) => n.name === control.value) ? { projectExist: true } : null); + }) + ); + }; +}; diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/PoolNameValidator.ts b/src/app/components/resource-pools-management/add-resource-pool-dialog/PoolNameValidator.ts new file mode 100644 index 00000000..3c4bf1e5 --- /dev/null +++ b/src/app/components/resource-pools-management/add-resource-pool-dialog/PoolNameValidator.ts @@ -0,0 +1,26 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { Injectable } from '@angular/core'; + +@Injectable() +export class PoolNameValidator { + get(poolName) { + const pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/); + + if (!pattern.test(poolName.value)) { + return null; + } + + return { invalidName: true }; + } +} diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.html b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.html new file mode 100644 index 00000000..d090ac2b --- /dev/null +++ b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.html @@ -0,0 +1,30 @@ +

Create new pool

+
+ + + Pool name is required + Pool name is incorrect + Pool with this name exists + +
+ +
+ + +
+ diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.scss b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.scss new file mode 100644 index 00000000..0ab6fbfb --- /dev/null +++ b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.scss @@ -0,0 +1,25 @@ +.file-name-form-field { + width: 100%; +} + +.project-snackbar { + background: #2196f3; +} + +.userList { + display: flex; + margin: 10px; + justify-content: space-between; + flex: 1 1 auto; +} + +.users { + display: flex; + height: 180px; + overflow: auto; + flex-direction: column; +} + +.button-div { + float: right; +} diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.spec.ts b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.spec.ts new file mode 100644 index 00000000..8588fdb4 --- /dev/null +++ b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddResourcePoolDialogComponent } from './add-resource-pool-dialog.component'; + +describe('AddResourcePoolDialogComponent', () => { + let component: AddResourcePoolDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AddResourcePoolDialogComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddResourcePoolDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.ts b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.ts new file mode 100644 index 00000000..7ad5aac2 --- /dev/null +++ b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.ts @@ -0,0 +1,67 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators} from "@angular/forms"; +import {Controller} from "@models/controller"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {PoolNameValidator} from "@components/resource-pools-management/add-resource-pool-dialog/PoolNameValidator"; +import {ResourcePoolsService} from "@services/resource-pools.service"; +import {ToasterService} from "@services/toaster.service"; +import {poolNameAsyncValidator} from "@components/resource-pools-management/add-resource-pool-dialog/PoolNameAsyncValidator"; + +@Component({ + selector: 'app-add-resource-pool-dialog', + templateUrl: './add-resource-pool-dialog.component.html', + styleUrls: ['./add-resource-pool-dialog.component.scss'], + providers: [PoolNameValidator] +}) +export class AddResourcePoolDialogComponent implements OnInit { + poolNameForm: UntypedFormGroup; + controller: Controller; + + constructor(private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { controller: Controller }, + private formBuilder: UntypedFormBuilder, + private poolNameValidator: PoolNameValidator, + private resourcePoolsService: ResourcePoolsService, + private toasterService: ToasterService) { + } + + ngOnInit(): void { + this.controller = this.data.controller; + this.poolNameForm = this.formBuilder.group({ + poolName: new UntypedFormControl( + null, + [Validators.required, this.poolNameValidator.get], + [poolNameAsyncValidator(this.data.controller, this.resourcePoolsService)] + ), + }); + } + + onKeyDown(event) { + if (event.key === 'Enter') { + this.onAddClick(); + } + } + + get form() { + return this.poolNameForm.controls; + } + + onAddClick() { + if (this.poolNameForm.invalid) { + return; + } + const poolName = this.poolNameForm.controls['poolName'].value; + + this.resourcePoolsService.add(this.controller, poolName) + .subscribe((pool) => { + this.dialogRef.close(true); + }, (error) => { + this.toasterService.error(`An error occur while trying to create new pool ${poolName}`); + this.dialogRef.close(false); + }); + } + + onNoClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.html b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.html new file mode 100644 index 00000000..09cf2728 --- /dev/null +++ b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.html @@ -0,0 +1,8 @@ +

Are you sure to delete pools named:

+

{{pool.name}}

+
+ + +
diff --git a/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.scss b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.scss new file mode 100644 index 00000000..1b0fdabd --- /dev/null +++ b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.scss @@ -0,0 +1,6 @@ +:host { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.spec.ts b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.spec.ts new file mode 100644 index 00000000..c4975ded --- /dev/null +++ b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeleteResourcePoolComponent } from './delete-resource-pool.component'; + +describe('DeleteResourcePoolComponent', () => { + let component: DeleteResourcePoolComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DeleteResourcePoolComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DeleteResourcePoolComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.ts b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.ts new file mode 100644 index 00000000..28481a62 --- /dev/null +++ b/src/app/components/resource-pools-management/delete-resource-pool/delete-resource-pool.component.ts @@ -0,0 +1,40 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ + +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; + +@Component({ + selector: 'app-delete-resource-pool', + templateUrl: './delete-resource-pool.component.html', + styleUrls: ['./delete-resource-pool.component.scss'] +}) +export class DeleteResourcePoolComponent implements OnInit { + + constructor(private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { pools: ResourcePool[] }) {} + + ngOnInit(): void { + } + + onCancel() { + this.dialogRef.close(); + } + + onDelete() { + this.dialogRef.close(true); + } + + +} diff --git a/src/app/components/resource-pools-management/resource-pools-filter.pipe.spec.ts b/src/app/components/resource-pools-management/resource-pools-filter.pipe.spec.ts new file mode 100644 index 00000000..24841ca0 --- /dev/null +++ b/src/app/components/resource-pools-management/resource-pools-filter.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ResourcePoolsFilterPipe } from './resource-pools-filter.pipe'; + +describe('ResourcePoolsFilterPipe', () => { + it('create an instance', () => { + const pipe = new ResourcePoolsFilterPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/components/resource-pools-management/resource-pools-filter.pipe.ts b/src/app/components/resource-pools-management/resource-pools-filter.pipe.ts new file mode 100644 index 00000000..d1b02d93 --- /dev/null +++ b/src/app/components/resource-pools-management/resource-pools-filter.pipe.ts @@ -0,0 +1,18 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; +import {MatTableDataSource} from "@angular/material/table"; + +@Pipe({ + name: 'resourcePoolsFilter' +}) +export class ResourcePoolsFilterPipe implements PipeTransform { + transform(resourcePool: MatTableDataSource, searchText: string): MatTableDataSource { + if (!searchText) { + return resourcePool; + } + + searchText = searchText.trim().toLowerCase(); + resourcePool.filter = searchText; + return resourcePool; + } +} diff --git a/src/app/components/resource-pools-management/resource-pools-management.component.html b/src/app/components/resource-pools-management/resource-pools-management.component.html new file mode 100644 index 00000000..3e7eb3f3 --- /dev/null +++ b/src/app/components/resource-pools-management/resource-pools-management.component.html @@ -0,0 +1,82 @@ +
+
+
+

Resource Pools management

+ + +
+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name{{element.name}} + Creation date {{element.created_at}} Last update {{element.updated_at}} + +
+ + +
+
+ +
+ +
+
diff --git a/src/app/components/resource-pools-management/resource-pools-management.component.scss b/src/app/components/resource-pools-management/resource-pools-management.component.scss new file mode 100644 index 00000000..f64f2d65 --- /dev/null +++ b/src/app/components/resource-pools-management/resource-pools-management.component.scss @@ -0,0 +1,26 @@ +table { + width: 100%; +} + +.full-width { + width: 940px; + margin-left: -470px; + left: 50%; +} + +.add-ressourcePool-button { + height: 40px; + width: 160px; + margin: 20px; +} + +.loader { + position: absolute; + margin: auto; + height: 175px; + bottom: 0; + left: 0; + right: 0; + top: 0; + width: 175px; +} diff --git a/src/app/components/resource-pools-management/resource-pools-management.component.spec.ts b/src/app/components/resource-pools-management/resource-pools-management.component.spec.ts new file mode 100644 index 00000000..9971efef --- /dev/null +++ b/src/app/components/resource-pools-management/resource-pools-management.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResourcePoolsManagementComponent } from './resource-pools-management.component'; + +describe('ResourcePoolsManagementComponent', () => { + let component: ResourcePoolsManagementComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ResourcePoolsManagementComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ResourcePoolsManagementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/resource-pools-management/resource-pools-management.component.ts b/src/app/components/resource-pools-management/resource-pools-management.component.ts new file mode 100644 index 00000000..4b0d5c53 --- /dev/null +++ b/src/app/components/resource-pools-management/resource-pools-management.component.ts @@ -0,0 +1,123 @@ +import {Component, OnInit, QueryList, ViewChildren} from '@angular/core'; +import {Controller} from "@models/controller"; +import {MatPaginator} from "@angular/material/paginator"; +import {MatSort} from "@angular/material/sort"; +import {SelectionModel} from "@angular/cdk/collections"; +import {MatTableDataSource} from "@angular/material/table"; +import {ActivatedRoute} from "@angular/router"; +import {ControllerService} from "@services/controller.service"; +import {ToasterService} from "@services/toaster.service"; +import {MatDialog} from "@angular/material/dialog"; +import {forkJoin} from "rxjs"; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; +import { + AddResourcePoolDialogComponent +} from "@components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component"; +import {DeleteResourcePoolComponent} from "@components/resource-pools-management/delete-resource-pool/delete-resource-pool.component"; +import {ResourcePoolsService} from "@services/resource-pools.service"; + +@Component({ + selector: 'app-resource-pools-management', + templateUrl: './resource-pools-management.component.html', + styleUrls: ['./resource-pools-management.component.scss'] +}) +export class ResourcePoolsManagementComponent implements OnInit { + controller: Controller; + + @ViewChildren('resourcePoolsPaginator') resourcePoolsPaginator: QueryList; + @ViewChildren('resourcePoolsSort') resourcePoolsSort: QueryList; + + public displayedColumns = ['select', 'name', 'created_at', 'updated_at', 'delete']; + selection = new SelectionModel(true, []); + resourcePools: ResourcePool[]; + dataSource = new MatTableDataSource(); + searchText: string; + isReady = false; + + constructor( + private route: ActivatedRoute, + private controllerService: ControllerService, + private toasterService: ToasterService, + public resourcePoolsService: ResourcePoolsService, + public dialog: MatDialog + ) { + } + + + ngOnInit(): void { + const controllerId = this.route.parent.snapshot.paramMap.get('controller_id'); + this.controllerService.get(+controllerId).then((controller: Controller) => { + this.controller = controller; + this.refresh(); + }); + } + + ngAfterViewInit() { + this.resourcePoolsPaginator.changes.subscribe((comps: QueryList ) => + { + this.dataSource.paginator = comps.first; + }); + this.resourcePoolsSort.changes.subscribe((comps: QueryList) => { + this.dataSource.sort = comps.first; + }); + this.dataSource.sortingDataAccessor = (item, property) => { + switch (property) { + case 'name': + return item[property] ? item[property].toLowerCase() : ''; + default: + return item[property]; + } + }; + } + + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.resourcePools.length; + return numSelected === numRows; + } + + masterToggle() { + this.isAllSelected() ? + this.selection.clear() : + this.resourcePools.forEach(row => this.selection.select(row)); + } + + addResourcePool() { + this.dialog + .open(AddResourcePoolDialogComponent, {width: '600px', height: '500px', data: {controller: this.controller}}) + .afterClosed() + .subscribe((added: boolean) => { + if (added) { + this.refresh(); + } + }); + } + + refresh() { + this.resourcePoolsService.getAll(this.controller).subscribe((resourcePools: ResourcePool[]) => { + this.isReady = true; + this.resourcePools = resourcePools; + this.dataSource.data = resourcePools; + this.selection.clear(); + }); + } + + onDelete(resourcePoolToDelete: ResourcePool[]) { + this.dialog + .open(DeleteResourcePoolComponent, {width: '500px', height: '250px', data: {pools: resourcePoolToDelete}}) + .afterClosed() + .subscribe((isDeletedConfirm) => { + if (isDeletedConfirm) { + const observables = resourcePoolToDelete.map((resourcePool: ResourcePool) => this.resourcePoolsService.delete(this.controller, resourcePool.resource_pool_id)); + forkJoin(observables) + .subscribe(() => { + this.refresh(); + }, + (error) => { + this.toasterService.error(`An error occur while trying to delete resource pool`); + }); + } + }); + } + +} diff --git a/src/app/models/resourcePools/Resource.ts b/src/app/models/resourcePools/Resource.ts new file mode 100644 index 00000000..17a98345 --- /dev/null +++ b/src/app/models/resourcePools/Resource.ts @@ -0,0 +1,7 @@ +export class Resource { + resource_id: string; + resource_type: string; + name: string; + created_at: string; + updated_at: string; +} diff --git a/src/app/models/resourcePools/ResourcePool.ts b/src/app/models/resourcePools/ResourcePool.ts new file mode 100644 index 00000000..eb9b4f52 --- /dev/null +++ b/src/app/models/resourcePools/ResourcePool.ts @@ -0,0 +1,9 @@ +import {Resource} from "@models/resourcePools/Resource"; + +export class ResourcePool { + name: string; + created_at: string; + updated_at: string; + resource_pool_id: string; + resources?: Resource[]; +} diff --git a/src/app/resolvers/resource-pools.resolver.spec.ts b/src/app/resolvers/resource-pools.resolver.spec.ts new file mode 100644 index 00000000..363edffa --- /dev/null +++ b/src/app/resolvers/resource-pools.resolver.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ResourcePoolsResolver } from './resource-pools.resolver'; + +describe('ResourcePoolsResolver', () => { + let resolver: ResourcePoolsResolver; + + beforeEach(() => { + TestBed.configureTestingModule({}); + resolver = TestBed.inject(ResourcePoolsResolver); + }); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); +}); diff --git a/src/app/resolvers/resource-pools.resolver.ts b/src/app/resolvers/resource-pools.resolver.ts new file mode 100644 index 00000000..c8cb8056 --- /dev/null +++ b/src/app/resolvers/resource-pools.resolver.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {ControllerService} from "@services/controller.service"; +import {ResourcePoolsService} from "@services/resource-pools.service"; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; +import {Controller} from "@models/controller"; + +@Injectable({ + providedIn: 'root' +}) +export class ResourcePoolsResolver implements Resolve { + + constructor(private controllerService: ControllerService, + private resourcePoolsService: ResourcePoolsService, + ) { + } + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return new Observable((subscriber: Subscriber) => { + + const controllerId = route.paramMap.get('controller_id'); + const poolId = route.paramMap.get('pool_id'); + + this.controllerService.get(+controllerId).then((controller: Controller) => { + this.resourcePoolsService.get(controller, poolId).subscribe((resourcePool: ResourcePool) => { + subscriber.next(resourcePool); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/services/resource-pools.service.spec.ts b/src/app/services/resource-pools.service.spec.ts new file mode 100644 index 00000000..4a4d7f10 --- /dev/null +++ b/src/app/services/resource-pools.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ResourcePoolsService } from './resource-pools.service'; + +describe('ResourcePoolsService', () => { + let service: ResourcePoolsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ResourcePoolsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/resource-pools.service.ts b/src/app/services/resource-pools.service.ts new file mode 100644 index 00000000..45a9a375 --- /dev/null +++ b/src/app/services/resource-pools.service.ts @@ -0,0 +1,88 @@ +import {Injectable} from '@angular/core'; +import {Controller} from "@models/controller"; +import {Observable, of} from "rxjs"; +import {ResourcePool} from "@models/resourcePools/ResourcePool"; +import {HttpController} from "@services/http-controller.service"; +import {Resource} from "@models/resourcePools/Resource"; +import {filter, map, mergeAll, switchMap, tap} from "rxjs/operators"; +import {Project} from "@models/project"; +import {ProjectService} from "@services/project.service"; + +@Injectable({ + providedIn: 'root' +}) +export class ResourcePoolsService { + + constructor(private httpController: HttpController, + private projectService: ProjectService) { + } + + getAll(controller: Controller) { + return this.httpController.get(controller, '/pools'); + } + + get(controller: Controller, poolId: string): Observable { + return Observable.forkJoin([ + this.httpController.get(controller, `/pools/${poolId}`), + this.httpController.get(controller, `/pools/${poolId}/resources`), + ]).pipe(map(results => { + results[0].resources = results[1]; + return results[0]; + })); + } + + delete(controller: Controller, uuid: string) { + return this.httpController.delete(controller, `/pools/${uuid}`); + } + + add(controller: Controller, newPoolName: string) { + return this.httpController.post<{ name: string }>(controller, '/pools', {name: newPoolName}); + } + + update(controller: Controller, pool: ResourcePool) { + return this.httpController.put(controller, `/pools/${pool.resource_pool_id}`, {name: pool.name}); + } + + addResource(controller: Controller, pool: ResourcePool, project: Project) { + return this.httpController.put(controller, `/pools/${pool.resource_pool_id}/resources/${project.project_id}`, {}); + } + + deleteResource(controller: Controller, resource: Resource, pool: ResourcePool) { + return this.httpController.delete(controller, `/pools/${pool.resource_pool_id}/resources/${resource.resource_id}`); + } + + getFreeResources(controller: Controller) { + return this.projectService + .list(controller) + .pipe( + switchMap((projects) => { + return this.getAllNonFreeResources(controller) + .pipe(map(resources => resources.map(resource => resource.resource_id)), + map(resources_id => projects.filter(project => !resources_id.includes(project.project_id))) + ) + })); + } + + private getAllNonFreeResources(controller: Controller) { + return this.getAll(controller) + .pipe(switchMap((resourcesPools) => { + return Observable.forkJoin( + resourcesPools.map((r) => this.httpController.get(controller, `/pools/${r.resource_pool_id}/resources`),) + ) + }), + map((data) => { + + //flatten results + + const output: Resource[] = []; + for(const res of data) { + for(const r of res) { + output.push(r); + } + } + + return output; + + })); + } +} diff --git a/src/app/services/toaster.service.ts b/src/app/services/toaster.service.ts index 266c2d32..105d2ed6 100644 --- a/src/app/services/toaster.service.ts +++ b/src/app/services/toaster.service.ts @@ -27,6 +27,7 @@ export class ToasterService { constructor(private snackbar: MatSnackBar, private zone: NgZone) {} public error(message: string) { + console.error(message); this.zone.run(() => { this.snackbar.open(message, 'Close', this.snackBarConfigForError); });