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 @@
+
+
+
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
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ 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);
});