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 @@ +
+
+
+

ACEs management

+ + +
+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + 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

+
+
+ + + + +
+ + {{node.name}} + +
+
+ + +
+ + {{node.name}} + +
+ +
+ +
+
+
+
+ + +
+
+
+ + {{t.charAt(0).toUpperCase() + t.slice(1)}} + +
+ + + + + + + + + + +
+
+ Propagate +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
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 ?

+
    +
  • {{ ace.path }}
  • +
+
+ + +
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 @@ +
+
+
+ + keyboard_arrow_left + +

role {{pool.name}} details

+
+
+
+
+ + pool name: + + +
+
creation date: {{pool.created_at}}
+
last update date: {{pool.updated_at}}
+
uuid: {{pool.resource_pool_id}}
+
+ +
+
+ +
+
+
+ + + + {{option}} + + +
+
+ +
+
+
+
{{resource.name}}
+
+ +
+
+
+
+
+
diff --git a/src/app/components/resource-pool-details/resource-pool-details.component.scss b/src/app/components/resource-pool-details/resource-pool-details.component.scss new file mode 100644 index 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

+
+ + + Pool name is required + Pool name is incorrect + Pool with this name exists + +
+ +
+ + +
+ diff --git a/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.scss b/src/app/components/resource-pools-management/add-resource-pool-dialog/add-resource-pool-dialog.component.scss new file mode 100644 index 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 @@ +
+
+
+

Resource Pools management

+ + +
+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name{{element.name}} + Creation date {{element.created_at}} Last update {{element.updated_at}} + +
+ + +
+
+ +
+ +
+
diff --git a/src/app/components/resource-pools-management/resource-pools-management.component.scss b/src/app/components/resource-pools-management/resource-pools-management.component.scss new file mode 100644 index 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); });