diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index b1a9bb0d5..7f6ea8975 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -69,6 +69,10 @@ 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 {AceManagementComponent} from "@components/ace-management/ace-management.component";
+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 +103,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 +244,14 @@ const routes: Routes = [
{
path: 'roles',
component: RoleManagementComponent
+ },
+ {
+ path: "resourcePools",
+ component: ResourcePoolsManagementComponent
+ },
+ {
+ path: 'aces',
+ component: AceManagementComponent
}
]
},
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index b83802a7f..f2ce3a99f 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -309,8 +309,22 @@ import { ExportPortableProjectComponent } from './components/export-portable-pro
import { NodesMenuConfirmationDialogComponent } from './components/project-map/nodes-menu/nodes-menu-confirmation-dialog/nodes-menu-confirmation-dialog.component';
import { ConfirmationDeleteAllProjectsComponent } from './components/projects/confirmation-delete-all-projects/confirmation-delete-all-projects.component';
import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component';
+import {AceManagementComponent} from "@components/ace-management/ace-management.component";
+import { AddAceDialogComponent } from './components/ace-management/add-ace-dialog/add-ace-dialog.component';
+import { AutocompleteComponent } from './components/ace-management/add-ace-dialog/autocomplete/autocomplete.component';
+import { DeleteAceDialogComponent } from './components/ace-management/delete-ace-dialog/delete-ace-dialog.component';
+import { AceFilterPipe } from './filters/ace-filter.pipe';
+import {CdkAccordionModule} from "@angular/cdk/accordion";
+import {CdkTreeModule} from "@angular/cdk/tree";
+
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: [
@@ -533,8 +547,19 @@ import { GroupPrivilegesPipe } from './components/role-management/role-detail/pr
NodesMenuConfirmationDialogComponent,
ConfirmationDeleteAllProjectsComponent,
ProjectMapLockConfirmationDialogComponent,
+ AceManagementComponent,
+ AddAceDialogComponent,
+ AutocompleteComponent,
+ DeleteAceDialogComponent,
+ AceFilterPipe,
PrivilegeComponent,
GroupPrivilegesPipe,
+ ResourcePoolsManagementComponent,
+ AddResourcePoolDialogComponent,
+ DeleteResourcePoolComponent,
+ ResourcePoolsFilterPipe,
+ ResourcePoolDetailsComponent,
+ DeleteResourceConfirmationDialogComponent,
],
imports: [
BrowserModule,
@@ -560,6 +585,8 @@ import { GroupPrivilegesPipe } from './components/role-management/role-detail/pr
MatSlideToggleModule,
MatCheckboxModule,
MatAutocompleteModule,
+ CdkAccordionModule,
+ CdkTreeModule,
],
providers: [
SettingsService,
diff --git a/src/app/components/ace-management/ace-management.component.html b/src/app/components/ace-management/ace-management.component.html
new file mode 100644
index 000000000..806b9d767
--- /dev/null
+++ b/src/app/components/ace-management/ace-management.component.html
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ Path |
+ {{getNameByUuidFromEndpoint(element.path)}} |
+
+
+
+ User/Group |
+
+ {{getNameByUuidFromEndpoint(element.user_id)}}
+ {{getNameByUuidFromEndpoint(element.group_id)}}
+ |
+
+
+
+ Role |
+ {{getNameByUuidFromEndpoint(element.role_id)}} |
+
+
+
+ Propagate |
+ {{element.propagate}} |
+
+
+
+ Allowed |
+ {{element.allowed}} |
+
+
+
+ Created |
+ {{element.created_at}} |
+
+
+
+ Last update |
+ {{element.updated_at}} |
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/ace-management/ace-management.component.scss b/src/app/components/ace-management/ace-management.component.scss
new file mode 100644
index 000000000..7fe53df53
--- /dev/null
+++ b/src/app/components/ace-management/ace-management.component.scss
@@ -0,0 +1,26 @@
+table {
+ width: 100%;
+}
+
+.full-width {
+ width: 940px;
+ margin-left: -470px;
+ left: 50%;
+}
+
+.add-ace-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/ace-management/ace-management.component.spec.ts b/src/app/components/ace-management/ace-management.component.spec.ts
new file mode 100644
index 000000000..c491e561b
--- /dev/null
+++ b/src/app/components/ace-management/ace-management.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AceManagementComponent } from './ace-management.component';
+
+describe('AceManagementComponent', () => {
+ let component: AceManagementComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ AceManagementComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AceManagementComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/ace-management/ace-management.component.ts b/src/app/components/ace-management/ace-management.component.ts
new file mode 100644
index 000000000..424086902
--- /dev/null
+++ b/src/app/components/ace-management/ace-management.component.ts
@@ -0,0 +1,176 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2023 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, OnInit, QueryList, ViewChildren} from '@angular/core';
+import {Controller} from "@models/controller";
+import {SelectionModel} from "@angular/cdk/collections";
+import {Group} from "@models/groups/group";
+import {MatTableDataSource} from "@angular/material/table";
+import {ACE} from "@models/api/ACE";
+import {ActivatedRoute} from "@angular/router";
+import {ControllerService} from "@services/controller.service";
+import {ToasterService} from "@services/toaster.service";
+import {GroupService} from "@services/group.service";
+import {MatDialog} from "@angular/material/dialog";
+import {AclService} from "@services/acl.service";
+import {MatPaginator} from "@angular/material/paginator";
+import {MatSort} from "@angular/material/sort";
+import {AddUserDialogComponent} from "@components/user-management/add-user-dialog/add-user-dialog.component";
+import {AddAceDialogComponent} from "@components/ace-management/add-ace-dialog/add-ace-dialog.component";
+import {DeleteUserDialogComponent} from "@components/user-management/delete-user-dialog/delete-user-dialog.component";
+import {DeleteAceDialogComponent} from "@components/ace-management/delete-ace-dialog/delete-ace-dialog.component";
+import {User} from "@models/users/user";
+import {Endpoint} from "@models/api/endpoint";
+
+@Component({
+ selector: 'app-ace-management',
+ templateUrl: './ace-management.component.html',
+ styleUrls: ['./ace-management.component.scss']
+})
+export class AceManagementComponent implements OnInit {
+
+
+ @ViewChildren('acesPaginator') acesPaginator: QueryList;
+ @ViewChildren('acesSort') acesSort: QueryList;
+ controller: Controller;
+ public displayedColumns = ['select', 'path', 'user/group', 'role', 'propagate', 'allowed', 'updated_at', 'delete'];
+ selection = new SelectionModel(true, []);
+ aces: ACE[];
+ dataSource = new MatTableDataSource();
+ isReady = false;
+ searchText = '';
+ endpoints: Endpoint[];
+
+ constructor(private route: ActivatedRoute,
+ private controllerService: ControllerService,
+ private toasterService: ToasterService,
+ public aclService: AclService,
+ 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.aclService.getEndpoints(this.controller)
+ .subscribe((endpoints: Endpoint[]) => {
+ this.endpoints = endpoints
+ this.refresh();
+ })
+ });
+
+
+ }
+
+ ngAfterViewInit() {
+ this.acesPaginator.changes.subscribe((comps: QueryList ) =>
+ {
+ this.dataSource.paginator = comps.first;
+ });
+
+ this.acesSort.changes.subscribe((comps: QueryList) => {
+ this.dataSource.sort = comps.first;
+ })
+
+ this.dataSource.sortingDataAccessor = (item, property) => {
+ switch (property) {
+ case 'path':
+ case 'user/group':
+ case 'role':
+ return item[property] ? item[property].toLowerCase() : '';
+ default:
+ return item[property];
+ }
+ };
+ }
+
+ refresh() {
+ this.aclService.list(this.controller).subscribe((aces: ACE[]) => {
+ this.isReady = true;
+ this.aces = aces
+ this.dataSource.data = aces;
+ this.selection.clear();
+ });
+ }
+
+ addACE() {
+ const dialogRef = this.dialog.open(AddAceDialogComponent, {
+ width: '1200px',
+ height: '500px',
+ autoFocus: false,
+ disableClose: true,
+ data: {endpoints: this.endpoints}
+ });
+ let instance = dialogRef.componentInstance;
+ instance.controller = this.controller;
+ dialogRef.afterClosed().subscribe(() => this.refresh());
+ }
+
+
+ onDelete(ace: ACE) {
+ this.dialog
+ .open(DeleteAceDialogComponent, {width: '500px', data: {aces: [ace]}})
+ .afterClosed()
+ .subscribe((isDeletedConfirm) => {
+ if (isDeletedConfirm) {
+ this.aclService.delete(this.controller, ace.ace_id)
+ .subscribe(() => {
+ this.refresh()
+ }, (error) => {
+ this.toasterService.error(`An error occur while trying to delete ace ${ace.ace_id}`);
+ });
+ }
+ });
+ }
+
+ isAllSelected() {
+ const numSelected = this.selection.selected.length;
+ const numRows = this.aces.length;
+ return numSelected === numRows;
+ }
+
+ masterToggle() {
+ this.isAllSelected() ?
+ this.selection.clear() :
+ this.aces.forEach(row => this.selection.select(row));
+ }
+
+ deleteMultiple() {
+ this.dialog
+ .open(DeleteAceDialogComponent, {width: '500px', data: {aces: this.selection.selected}})
+ .afterClosed()
+ .subscribe((isDeletedConfirm) => {
+ if (isDeletedConfirm) {
+ this.selection.selected.forEach((ace: ACE) => {
+ this.aclService.delete(this.controller, ace.ace_id)
+ .subscribe(() => {
+ this.refresh()
+ }, (error) => {
+ this.toasterService.error(`An error occur while trying to delete ace ${ace.ace_id}`);
+ });
+ })
+ this.selection.clear();
+ }
+ });
+ }
+
+ getNameByUuidFromEndpoint(uuid: string): string {
+ if (this.endpoints) {
+ const elt = this.endpoints.filter((endpoint: Endpoint) => endpoint.endpoint.includes(uuid))
+ if (elt.length >= 1) {
+ return elt[0].name
+ }
+ }
+
+ return ''
+ }
+}
diff --git a/src/app/components/ace-management/add-ace-dialog/EndpointTreeAdapter.spec.ts b/src/app/components/ace-management/add-ace-dialog/EndpointTreeAdapter.spec.ts
new file mode 100644
index 000000000..8597edc37
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/EndpointTreeAdapter.spec.ts
@@ -0,0 +1,71 @@
+import {EndpointTreeAdapter} from "@components/ace-management/add-ace-dialog/EndpointTreeAdapter";
+import {Endpoint, RessourceType} from "@models/api/endpoint";
+
+const endpoint1: Endpoint = {
+ endpoint: "/",
+ endpoint_type: RessourceType.image,
+ name: "Root"
+}
+
+const endpoint2: Endpoint = {
+ endpoint: "/projects",
+ endpoint_type: RessourceType.project,
+ name: "All projects"
+
+}
+
+const endpoint3: Endpoint = {
+ endpoint: "/images",
+ endpoint_type: RessourceType.image,
+ name: "All images"
+
+}
+
+const endpoint4: Endpoint = {
+ endpoint: "/projects/blabla",
+ endpoint_type: RessourceType.project,
+ name: "Project blabla"
+
+}
+
+const endpoint5 : Endpoint = {
+ endpoint: "/projects/blabla/nodes",
+ endpoint_type: RessourceType.node,
+ name: "All nodes for project blabla"
+}
+
+const endpoint6 : Endpoint = {
+ endpoint: "/images/blabla",
+ endpoint_type: RessourceType.image,
+ name: "Image blabla"
+}
+
+
+let endpoints: Endpoint[] = [endpoint1, endpoint2, endpoint3, endpoint4, endpoint5, endpoint6];
+
+describe('EndpointTreeAdapter', () => {
+
+ it('Should build endpointTree', () => {
+
+ const adapter = new EndpointTreeAdapter(endpoints);
+ const tree = adapter.buildTreeFromEndpoints()
+ expect(tree.length).toEqual(1);
+ expect(tree[0].children.length).toEqual(2);
+
+ const projectEndpoint = tree[0].children[0];
+
+ expect(projectEndpoint.children.length).toEqual(1);
+ expect(projectEndpoint.children[0].children.length).toEqual(1);
+
+ const imageEndpoint = tree[0].children[0];
+ expect(imageEndpoint.children.length).toEqual(1)
+ expect(imageEndpoint.children[0].children.length).toEqual(0);
+ });
+
+ it('Should build empty tree', () => {
+ const adapter = new EndpointTreeAdapter([]);
+ const tree = adapter.buildTreeFromEndpoints()
+
+ expect(tree.length).toEqual(0);
+ })
+})
diff --git a/src/app/components/ace-management/add-ace-dialog/EndpointTreeAdapter.ts b/src/app/components/ace-management/add-ace-dialog/EndpointTreeAdapter.ts
new file mode 100644
index 000000000..e021d9d67
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/EndpointTreeAdapter.ts
@@ -0,0 +1,59 @@
+import {Endpoint, RessourceType} from "../../../models/api/endpoint";
+
+export interface EndpointNode {
+ endpoint: string;
+ name: string;
+ endpoint_type: RessourceType;
+ depth: number;
+ splitEndp: string[];
+ parent?: string[];
+ children?: EndpointNode[];
+}
+
+export class EndpointTreeAdapter {
+ private endpoints: Endpoint[]
+
+ constructor(endpoints: Endpoint[]) {
+ this.endpoints = endpoints
+ }
+
+ buildTreeFromEndpoints(): EndpointNode[] {
+ const parentNode: EndpointNode[] = []
+ let nodes = []
+ this.endpoints.forEach((endp: Endpoint) => {
+ const node = this.extractParent(endp)
+ nodes.push(node)
+ })
+
+ nodes.forEach((node: EndpointNode) => {
+ if(node.depth > 0) {
+ const parent = nodes.filter((n: EndpointNode) => n.splitEndp.join('/') == node.splitEndp.slice(0, node.depth-1).join('/'))[0]
+ parent.children.push(node)
+ }
+ })
+
+ parentNode.push(nodes.find((node: EndpointNode) => node.depth === 0))
+
+ return parentNode
+ }
+
+ private extractParent(endp: Endpoint): EndpointNode {
+
+ let splitEndp = endp.endpoint.split('/');
+ splitEndp = splitEndp.filter((value: string) => value !== '' && value !== 'access')
+ let parent = [];
+ if (splitEndp.length > 0) {
+ parent = splitEndp.slice(0,splitEndp.length - 1)
+ }
+ const node: EndpointNode = {
+ children: [],
+ depth: splitEndp.length,
+ splitEndp: splitEndp,
+ endpoint: endp.endpoint,
+ endpoint_type: endp.endpoint_type,
+ name: endp.name,
+ parent: parent
+ }
+ return node
+ }
+}
diff --git a/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.html b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.html
new file mode 100644
index 000000000..8bc8e927a
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.html
@@ -0,0 +1,108 @@
+Create new ACE
+
diff --git a/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.scss b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.scss
new file mode 100644
index 000000000..2521641cc
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.scss
@@ -0,0 +1,71 @@
+.input-field {
+ width: 100%;
+}
+
+.height-100 {
+ height: 100%;
+}
+
+.button-div {
+ float: right;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+}
+
+.allow {
+ background-color: green;
+}
+
+.deny {
+ background-color: darkred;
+}
+
+.typeSelect {
+ height: 25px;
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.groupList {
+ display: flex;
+ margin: 10px;
+ justify-content: space-between;
+ flex: 1 1 auto
+}
+
+.groups {
+ display: flex;
+ height: 180px;
+ overflow: auto;
+ flex-direction: column;
+}
+
+.example-tree-invisible {
+ display: none;
+}
+
+.example-tree ul,
+.example-tree li {
+ margin-top: 0;
+ margin-bottom: 0;
+ list-style-type: none;
+}
+.example-tree-node {
+ display: block;
+}
+
+.example-tree-node .example-tree-node {
+ padding-left: 40px;
+}
+
+.form-div {
+ position: relative;
+ width: 25%;
+ float:right;
+ margin-left: 20px;
+}
+
+.selected {
+ color: green
+}
diff --git a/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.spec.ts b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.spec.ts
new file mode 100644
index 000000000..c6627e6a0
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AddAceDialogComponent } from './add-ace-dialog.component';
+
+describe('AddAceDialogComponent', () => {
+ let component: AddAceDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ AddAceDialogComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AddAceDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.ts b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.ts
new file mode 100644
index 000000000..2bd4959c1
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/add-ace-dialog.component.ts
@@ -0,0 +1,191 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2023 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 {UserService} from "@services/user.service";
+import {ToasterService} from "@services/toaster.service";
+import {AclService} from "@services/acl.service";
+import {Controller} from "@models/controller";
+import {Endpoint, RessourceType} from "@models/api/endpoint";
+import {UntypedFormControl, UntypedFormGroup} from "@angular/forms";
+import {ACE, AceType} from "@models/api/ACE";
+import {Group} from "@models/groups/group";
+import {GroupService} from "@services/group.service";
+import {User} from "@models/users/user";
+import {Role} from "@models/api/role";
+import {RoleService} from "@services/role.service";
+import {NestedTreeControl} from "@angular/cdk/tree";
+import {ArrayDataSource} from "@angular/cdk/collections";
+import {EndpointNode, EndpointTreeAdapter} from "@components/ace-management/add-ace-dialog/EndpointTreeAdapter";
+
+
+
+@Component({
+ selector: 'app-add-ace-dialog',
+ templateUrl: './add-ace-dialog.component.html',
+ styleUrls: ['./add-ace-dialog.component.scss']
+})
+export class AddAceDialogComponent implements OnInit {
+ controller: Controller
+ addAceForm: UntypedFormGroup
+ allowed: boolean = true
+ types = Object.values(AceType);
+
+ endpoints: Endpoint[];
+ selectedEndpoint: Endpoint
+ filteredEndpoint: Endpoint[]
+ endpointTypes: string[]
+
+ groups: Group[];
+ selectedGroup: Group;
+
+ users: User[];
+ selectedUser: User;
+
+ roles: Role[];
+ selectedRole: Role;
+
+ TREE_DATA: EndpointNode[] = [];
+ treeControl = new NestedTreeControl(node => node.children);
+ treeDataSource: ArrayDataSource ;
+
+ constructor(public dialogRef: MatDialogRef,
+ public aclService: AclService,
+ public userService: UserService,
+ private groupService: GroupService,
+ private roleService: RoleService,
+ private toasterService: ToasterService,
+ @Inject(MAT_DIALOG_DATA) public data: { endpoints: Endpoint[] }) {
+ this.endpoints = data.endpoints
+ const treeAdapter = new EndpointTreeAdapter(this.endpoints)
+ const data_tree = treeAdapter.buildTreeFromEndpoints()
+ this.treeDataSource = new ArrayDataSource(data_tree);
+ console.log(data_tree)
+ }
+
+ ngOnInit(): void {
+ this.addAceForm = new UntypedFormGroup({
+ type: new UntypedFormControl(AceType.user),
+ role_id: new UntypedFormControl(),
+ propagate: new UntypedFormControl(true)
+ });
+ this.groupService.getGroups(this.controller)
+ .subscribe((groups: Group[]) => {
+ this.groups = groups;
+ })
+ this.userService.list(this.controller)
+ .subscribe((users: User[]) => {
+ this.users = users;
+ })
+ this.roleService.get(this.controller)
+ .subscribe((roles: Role[]) => {
+ this.roles = roles;
+ })
+
+ }
+
+ get form() {
+ return this.addAceForm.controls;
+ }
+
+ onCancelClick() {
+ this.dialogRef.close();
+ }
+
+ onAddClick() {
+ const ACE = {
+ ace_type: this.form.type.value,
+ allowed: this.allowed,
+ group_id: this.form.type.value === AceType.group ? this.selectedGroup.user_group_id : null,
+ path: this.selectedEndpoint.endpoint,
+ propagate: this.form.propagate.value,
+ role_id: this.selectedRole.role_id,
+ user_id: this.form.type.value === AceType.user ? this.selectedUser.user_id : null,
+ }
+
+ if (ACE.path && ACE.role_id && (ACE.user_id || ACE.group_id)) {
+ this.aclService.add(this.controller, ACE)
+ .subscribe((ace: ACE) => {
+ this.toasterService.success(`ACE was added for path ${ACE.path}`);
+ },
+ (error) => {
+ this.toasterService.error(`Cannot create ACE : ${error.error.message}`)
+ })
+ this.dialogRef.close();
+ }
+ }
+
+ changeAllowed() {
+ this.allowed = !this.allowed
+ }
+
+ displayFn(value): string {
+ return value && value.name ? value.name : '';
+ }
+
+ displayFnUser(value): string {
+ return value && value.full_name && value.username ? value.username.concat(" - ", value.full_name) : '';
+ }
+
+ _filter(value: string, data: any): any {
+ if (typeof value === 'string' && data) {
+ const filterValue = value.toLowerCase();
+
+ return data.filter(option => option.name.toLowerCase().includes(filterValue));
+ }
+
+ }
+
+ _filterUser(value: string, users: User[]): User[] {
+ if (typeof value === 'string' && users) {
+ const filterValue = value.toLowerCase();
+
+ return users.filter(option => option.full_name.toLowerCase().includes(filterValue) || option.username.toLowerCase().includes(filterValue));
+ }
+
+ }
+
+ _filterRole(value: string, roles: Role[]) {
+ if (typeof value === 'string' && roles) {
+ const filterValue = value.toLowerCase();
+
+ return roles.filter(option => option.name.toLowerCase().includes(filterValue));
+ }
+ }
+
+ userSelection(value: any) {
+ this.selectedUser = value
+ }
+
+ groupSelection(value: any) {
+ this.selectedGroup = value
+ }
+
+ roleSelection(value: any) {
+ this.selectedRole = value;
+ }
+
+ endpointSelection(value: EndpointNode) {
+ const endp: Endpoint = {
+ endpoint: value.endpoint,
+ endpoint_type: value.endpoint_type,
+ name: value.name
+ }
+ this.selectedEndpoint = endp
+ }
+
+ hasChild = (_: number, node: EndpointNode) => !!node.children && node.children.length > 0;
+
+
+}
diff --git a/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.html b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.html
new file mode 100644
index 000000000..6f1f6bef5
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.html
@@ -0,0 +1,12 @@
+
+ {{eltType}}
+
+
+
+ {{displayFn(elt)}}
+
+
+
diff --git a/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.scss b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.scss
new file mode 100644
index 000000000..2888a7487
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.scss
@@ -0,0 +1,3 @@
+.input-field {
+ width: 100%;
+}
diff --git a/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.spec.ts b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.spec.ts
new file mode 100644
index 000000000..264fb48bb
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AutocompleteComponent } from './autocomplete.component';
+
+describe('AutocompleteComponent', () => {
+ let component: AutocompleteComponent;
+ let fixture: ComponentFixture>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ AutocompleteComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AutocompleteComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.ts b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.ts
new file mode 100644
index 000000000..14850501c
--- /dev/null
+++ b/src/app/components/ace-management/add-ace-dialog/autocomplete/autocomplete.component.ts
@@ -0,0 +1,34 @@
+import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
+import {Group} from "@models/groups/group";
+import {Observable} from "rxjs";
+import {UntypedFormControl} from "@angular/forms";
+import {map, startWith} from "rxjs/operators";
+import {data} from "autoprefixer";
+
+@Component({
+ selector: 'app-autocomplete',
+ templateUrl: './autocomplete.component.html',
+ styleUrls: ['./autocomplete.component.scss']
+})
+export class AutocompleteComponent implements OnChanges {
+
+ @Input() data: T[];
+ filteredData: Observable;
+ typeName: string
+ autocompleteControl = new UntypedFormControl();
+
+ @Input() eltType: string
+ @Input() displayFn: (value: T) => string
+ @Input() filterFn: (value: string, data: T[]) => T[]
+ @Output() onSelection: EventEmitter = new EventEmitter();
+
+ constructor() { }
+
+ ngOnChanges(): void {
+ this.filteredData = this.autocompleteControl.valueChanges.pipe(
+ startWith(''),
+ map(value => this.filterFn(value, this.data))
+ )
+ }
+
+}
diff --git a/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.html b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.html
new file mode 100644
index 000000000..5206106d4
--- /dev/null
+++ b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.html
@@ -0,0 +1,10 @@
+Are you sure you want to delete the following ACEs ?
+
+
+
+
+
diff --git a/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.scss b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.spec.ts b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.spec.ts
new file mode 100644
index 000000000..47d8ac18b
--- /dev/null
+++ b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeleteAceDialogComponent } from './delete-ace-dialog.component';
+
+describe('DeleteAceDialogComponent', () => {
+ let component: DeleteAceDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ DeleteAceDialogComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(DeleteAceDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.ts b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.ts
new file mode 100644
index 000000000..43bbcdab3
--- /dev/null
+++ b/src/app/components/ace-management/delete-ace-dialog/delete-ace-dialog.component.ts
@@ -0,0 +1,38 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2023 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 {ACE} from "@models/api/ACE";
+
+@Component({
+ selector: 'app-delete-ace-dialog',
+ templateUrl: './delete-ace-dialog.component.html',
+ styleUrls: ['./delete-ace-dialog.component.scss']
+})
+export class DeleteAceDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { aces: ACE[] }) { }
+
+ ngOnInit(): void {
+ }
+
+ onCancel() {
+ this.dialogRef.close();
+ }
+
+ onDelete() {
+ this.dialogRef.close(true);
+ }
+}
diff --git a/src/app/components/management/management.component.ts b/src/app/components/management/management.component.ts
index 4ec7311cb..91e90e0ff 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', 'aces'];
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 000000000..3fb7aab99
--- /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 000000000..6d80719b6
--- /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 000000000..9a334b73b
--- /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 000000000..8ebeed133
--- /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 000000000..bc2a77cc0
--- /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 000000000..309df1a2c
--- /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 000000000..f050718dd
--- /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 000000000..82f3385e5
--- /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 000000000..94eef5d7e
--- /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 000000000..3c4bf1e5e
--- /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 000000000..d090ac2bd
--- /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 000000000..0ab6fbfb1
--- /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 000000000..8588fdb4b
--- /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 000000000..7ad5aac20
--- /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 000000000..09cf2728f
--- /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 000000000..1b0fdabd0
--- /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 000000000..c4975ded8
--- /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 000000000..28481a620
--- /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 000000000..24841ca0f
--- /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 000000000..d1b02d939
--- /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 000000000..3e7eb3f39
--- /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 000000000..f64f2d65f
--- /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 000000000..9971efef6
--- /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 000000000..4b0d5c53a
--- /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/filters/ace-filter.pipe.ts b/src/app/filters/ace-filter.pipe.ts
new file mode 100644
index 000000000..9b7495125
--- /dev/null
+++ b/src/app/filters/ace-filter.pipe.ts
@@ -0,0 +1,34 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import {MatTableDataSource} from "@angular/material/table";
+import {User} from "@models/users/user";
+import {ACE} from "@models/api/ACE";
+import {Endpoint} from "@models/api/endpoint";
+
+@Pipe({
+ name: 'aceFilter'
+})
+export class AceFilterPipe implements PipeTransform {
+
+ transform(items: MatTableDataSource, searchText: string, endpoints: Endpoint[]){
+ if (!items) return [];
+ if (!searchText) return items;
+ searchText = searchText.toLowerCase()
+ const filteredEndpoints = endpoints.filter((endp: Endpoint) => endp.name.toLowerCase().includes(searchText))
+ return items.data.filter((item: ACE) => {
+ const user = this.getEndpoint(item.user_id, endpoints)
+ const group = this.getEndpoint(item.group_id, endpoints)
+ const path = this.getEndpoint(item.path, endpoints)
+ const role = this.getEndpoint(item.role_id, endpoints)
+ return filteredEndpoints.some((endp: Endpoint) => [user, group, path, role].includes(endp.endpoint))
+ })
+
+ }
+
+ private getEndpoint(id: string, endpoints: Endpoint[]): string {
+ const filter = endpoints.filter((endpoint: Endpoint) => endpoint.endpoint.includes(id))
+ if(filter.length > 0) {
+ return filter[0].endpoint.toLowerCase()
+ }
+ return ''
+ }
+}
diff --git a/src/app/models/api/ACE.ts b/src/app/models/api/ACE.ts
new file mode 100644
index 000000000..dab2ebcc8
--- /dev/null
+++ b/src/app/models/api/ACE.ts
@@ -0,0 +1,18 @@
+export enum AceType {
+ group= "group",
+ user = "user"
+}
+
+
+export interface ACE {
+ ace_id: string;
+ ace_type: AceType;
+ path: string;
+ propagate: boolean;
+ allowed: boolean;
+ user_id?: string;
+ group_id?: string;
+ role_id: string;
+ created_at: string;
+ updated_at: string;
+}
diff --git a/src/app/models/api/endpoint.ts b/src/app/models/api/endpoint.ts
new file mode 100644
index 000000000..f83e2269d
--- /dev/null
+++ b/src/app/models/api/endpoint.ts
@@ -0,0 +1,17 @@
+export enum RessourceType {
+ project = "project",
+ node = "node",
+ link = "link",
+ user = "user",
+ group = "group",
+ pool = "pool",
+ image = "image",
+ template = "template",
+ root = "root"
+}
+
+export interface Endpoint {
+ endpoint: string,
+ name: string,
+ endpoint_type: RessourceType
+}
diff --git a/src/app/models/resourcePools/Resource.ts b/src/app/models/resourcePools/Resource.ts
new file mode 100644
index 000000000..17a98345a
--- /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 000000000..eb9b4f520
--- /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 000000000..363edffa3
--- /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 000000000..c8cb80562
--- /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/acl.service.spec.ts b/src/app/services/acl.service.spec.ts
new file mode 100644
index 000000000..48300a54b
--- /dev/null
+++ b/src/app/services/acl.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AclService } from './acl.service';
+
+describe('AclService', () => {
+ let service: AclService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(AclService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/acl.service.ts b/src/app/services/acl.service.ts
new file mode 100644
index 000000000..5b2a3e3f9
--- /dev/null
+++ b/src/app/services/acl.service.ts
@@ -0,0 +1,53 @@
+/*
+* 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';
+import {Controller} from "@models/controller";
+import {Observable} from "rxjs";
+import {HttpController} from "@services/http-controller.service";
+import {ACE} from "@models/api/ACE";
+import {Endpoint} from "@models/api/endpoint";
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AclService {
+
+ constructor(
+ private httpController: HttpController
+ ) {}
+
+ getEndpoints(controller: Controller) {
+ return this.httpController.get(controller, '/access/acl/endpoints')
+ }
+
+ list(controller: Controller) {
+ return this.httpController.get(controller, '/access/acl');
+ }
+
+ add(controller: Controller, ace: any): Observable {
+ return this.httpController.post(controller, `/access/acl`, ace);
+ }
+
+ get(controller: Controller, ace_id: string) {
+ return this.httpController.get(controller, `/access/acl/${ace_id}`);
+ }
+
+ delete(controller: Controller, ace_id: string) {
+ return this.httpController.delete(controller, `/access/acl/${ace_id}`);
+ }
+
+ update(controller: Controller, ace: ACE): Observable {
+ return this.httpController.put(controller, `/access/acl/${ace.ace_id}`, ace);
+ }
+}
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 000000000..4a4d7f104
--- /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 000000000..45a9a3757
--- /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 266c2d32a..105d2ed61 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);
});