mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-06-24 17:15:22 +00:00
Compare commits
19 Commits
bugfix-133
...
v3.0.0a4
Author | SHA1 | Date | |
---|---|---|---|
97d054bb2b | |||
931b7b1109 | |||
c7b5d0d2f8 | |||
a71014de81 | |||
73c9fd3181 | |||
ff38d7abb7 | |||
db66a09e3b | |||
c517e98bb1 | |||
ed4fd7c06a | |||
4870e58977 | |||
6b5b784658 | |||
963af81d30 | |||
b106f31b36 | |||
39401f3184 | |||
55c993df71 | |||
2c0cd88ca6 | |||
9ca10d6d6a | |||
7bad625fdd | |||
bae7bcdd8d |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gns3-web-ui",
|
||||
"version": "3.0.0.dev7",
|
||||
"version": "3.0.0a4",
|
||||
"author": {
|
||||
"name": "GNS3 Technology Inc.",
|
||||
"email": "developers@gns3.com"
|
||||
@ -137,4 +137,4 @@
|
||||
]
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
}
|
||||
|
@ -60,20 +60,21 @@ import { ImageManagerComponent } from './components/image-manager/image-manager.
|
||||
import { UserDetailComponent } from "./components/user-management/user-detail/user-detail.component";
|
||||
import { UserDetailResolver } from "./resolvers/user-detail.resolver";
|
||||
import { ManagementComponent } from "./components/management/management.component";
|
||||
import { PermissionResolver } from "./resolvers/permission.resolver";
|
||||
import { UserGroupsResolver } from "./resolvers/user-groups.resolver";
|
||||
import { UserPermissionsResolver } from "./resolvers/user-permissions.resolver";
|
||||
import { GroupManagementComponent } from "./components/group-management/group-management.component";
|
||||
import { RoleManagementComponent } from "./components/role-management/role-management.component";
|
||||
import { PermissionsManagementComponent } from "./components/permissions-management/permissions-management.component";
|
||||
import { GroupDetailsComponent } from "./components/group-details/group-details.component";
|
||||
import { GroupMembersResolver } from "./resolvers/group-members.resolver";
|
||||
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 { RolePermissionsComponent } from "./components/role-management/role-detail/role-permissions/role-permissions.component";
|
||||
import { UserPermissionsComponent } from "./components/user-management/user-detail/user-permissions/user-permissions.component";
|
||||
import { AclManagementComponent } from "@components/acl-management/acl-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";
|
||||
import { GroupAcesResolver } from "@resolvers/group-ace.resolver.ts.resolver";
|
||||
import { UserAcesResolver } from "@resolvers/user-aces.resolver";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -102,9 +103,18 @@ const routes: Routes = [
|
||||
resolve: {
|
||||
user: UserDetailResolver,
|
||||
groups: UserGroupsResolver,
|
||||
permissions: UserPermissionsResolver,
|
||||
aces: UserAcesResolver,
|
||||
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] },
|
||||
|
||||
@ -239,8 +249,12 @@ const routes: Routes = [
|
||||
component: RoleManagementComponent
|
||||
},
|
||||
{
|
||||
path: 'permissions',
|
||||
component: PermissionsManagementComponent
|
||||
path: "pools",
|
||||
component: ResourcePoolsManagementComponent
|
||||
},
|
||||
{
|
||||
path: 'ACL',
|
||||
component: AclManagementComponent
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -251,7 +265,7 @@ const routes: Routes = [
|
||||
members: GroupMembersResolver,
|
||||
controller: ControllerResolve,
|
||||
group: GroupResolver,
|
||||
roles: GroupRoleResolver
|
||||
aces: GroupAcesResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -262,25 +276,6 @@ const routes: Routes = [
|
||||
controller: ControllerResolve
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'controller/:controller_id/management/roles/:role_id/permissions',
|
||||
component: RolePermissionsComponent,
|
||||
resolve: {
|
||||
role: RoleDetailResolver,
|
||||
controller: ControllerResolve,
|
||||
permissions: PermissionResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'controller/:controller_id/management/users/:user_id/permissions',
|
||||
component: UserPermissionsComponent,
|
||||
resolve: {
|
||||
user: UserDetailResolver,
|
||||
userPermissions: UserPermissionsResolver,
|
||||
controller: ControllerResolve,
|
||||
permissions: PermissionResolver
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -295,25 +295,10 @@ import { RoleFilterPipe } from './components/role-management/role-filter.pipe';
|
||||
import { AddRoleDialogComponent } from './components/role-management/add-role-dialog/add-role-dialog.component';
|
||||
import { DeleteRoleDialogComponent } from './components/role-management/delete-role-dialog/delete-role-dialog.component';
|
||||
import { RoleDetailComponent } from './components/role-management/role-detail/role-detail.component';
|
||||
import { PermissionEditorComponent } from './components/role-management/role-detail/permission-editor/permission-editor.component';
|
||||
import { EditablePermissionComponent } from './components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component';
|
||||
import { PermissionEditorValidateDialogComponent } from './components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component';
|
||||
import { PermissionsManagementComponent } from './components/permissions-management/permissions-management.component';
|
||||
import { PermissionEditLineComponent } from '@components/permissions-management/permission-edit-line/permission-edit-line.component';
|
||||
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
|
||||
import { UserPermissionsComponent } from './components/user-management/user-detail/user-permissions/user-permissions.component';
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {PathAutoCompleteComponent} from './components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component';
|
||||
import {FilterCompletePipe} from './components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe';
|
||||
import { AddPermissionLineComponent } from './components/permissions-management/add-permission-line/add-permission-line.component';
|
||||
import { MethodButtonComponent } from './components/permissions-management/method-button/method-button.component';
|
||||
import { ActionButtonComponent } from './components/permissions-management/action-button/action-button.component';
|
||||
import { DeletePermissionDialogComponent } from './components/permissions-management/delete-permission-dialog/delete-permission-dialog.component';
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";;
|
||||
import { AddRoleToGroupComponent } from './components/group-details/add-role-to-group/add-role-to-group.component';
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import { PermissionsFilterPipe } from './components/permissions-management/permissions-filter.pipe';
|
||||
import { DisplayPathPipe } from './components/permissions-management/display-path.pipe';
|
||||
import {RolePermissionsComponent} from "@components/role-management/role-detail/role-permissions/role-permissions.component";
|
||||
import { ChangeUserPasswordComponent } from './components/user-management/user-detail/change-user-password/change-user-password.component';
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import { ImageManagerComponent } from './components/image-manager/image-manager.component';
|
||||
@ -324,6 +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 { AclManagementComponent } from "@components/acl-management/acl-management.component";
|
||||
import { AddAceDialogComponent } from './components/acl-management/add-ace-dialog/add-ace-dialog.component';
|
||||
import { AutocompleteComponent } from './components/acl-management/add-ace-dialog/autocomplete/autocomplete.component';
|
||||
import { DeleteAceDialogComponent } from './components/acl-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,26 +534,9 @@ import { ProjectMapLockConfirmationDialogComponent } from './components/project-
|
||||
AddRoleDialogComponent,
|
||||
DeleteRoleDialogComponent,
|
||||
RoleDetailComponent,
|
||||
PermissionEditorComponent,
|
||||
EditablePermissionComponent,
|
||||
PermissionEditorValidateDialogComponent,
|
||||
RemoveToGroupDialogComponent,
|
||||
PermissionsManagementComponent,
|
||||
AddRoleToGroupComponent,
|
||||
PermissionEditLineComponent,
|
||||
AddPermissionLineComponent,
|
||||
MethodButtonComponent,
|
||||
ActionButtonComponent,
|
||||
DeletePermissionDialogComponent,
|
||||
PathAutoCompleteComponent,
|
||||
FilterCompletePipe,
|
||||
UserPermissionsComponent,
|
||||
PermissionsFilterPipe,
|
||||
RolePermissionsComponent,
|
||||
DisplayPathPipe,
|
||||
ChangeUserPasswordComponent,
|
||||
FilterCompletePipe,
|
||||
DisplayPathPipe,
|
||||
ChangeUserPasswordComponent,
|
||||
ProjectReadmeComponent,
|
||||
ImageManagerComponent,
|
||||
@ -563,6 +547,19 @@ import { ProjectMapLockConfirmationDialogComponent } from './components/project-
|
||||
NodesMenuConfirmationDialogComponent,
|
||||
ConfirmationDeleteAllProjectsComponent,
|
||||
ProjectMapLockConfirmationDialogComponent,
|
||||
AclManagementComponent,
|
||||
AddAceDialogComponent,
|
||||
AutocompleteComponent,
|
||||
DeleteAceDialogComponent,
|
||||
AceFilterPipe,
|
||||
PrivilegeComponent,
|
||||
GroupPrivilegesPipe,
|
||||
ResourcePoolsManagementComponent,
|
||||
AddResourcePoolDialogComponent,
|
||||
DeleteResourcePoolComponent,
|
||||
ResourcePoolsFilterPipe,
|
||||
ResourcePoolDetailsComponent,
|
||||
DeleteResourceConfirmationDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -588,6 +585,8 @@ import { ProjectMapLockConfirmationDialogComponent } from './components/project-
|
||||
MatSlideToggleModule,
|
||||
MatCheckboxModule,
|
||||
MatAutocompleteModule,
|
||||
CdkAccordionModule,
|
||||
CdkTreeModule,
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
|
@ -0,0 +1,98 @@
|
||||
<div class="content" *ngIf="isReady; else loading">
|
||||
<div class="default-header">
|
||||
<div class="row">
|
||||
<h1 class="col">Access Control List (ACL)</h1>
|
||||
<button class="col" mat-raised-button color="primary" (click)="deleteMultiple()" class="add-ace-button" [disabled]="selection.selected.length == 0">
|
||||
Delete selected ACEs
|
||||
</button>
|
||||
<button class="col" mat-raised-button color="primary" (click)="addACE()" class="add-ace-button">
|
||||
Add ACE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Search by path, user/group or role" [(ngModel)]="searchText"
|
||||
[ngModelOptions]="{ standalone: true }"/>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<div class="default-content">
|
||||
<table mat-table [dataSource]="dataSource | aceFilter: searchText:endpoints " class="mat-elevation-z8" matSort #acesSort="matSort">
|
||||
|
||||
<ng-container matColumnDef="select" >
|
||||
<th mat-header-cell *matHeaderCellDef class="small-col">
|
||||
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row" class="small-col">
|
||||
<mat-checkbox (click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(row) : null"
|
||||
[checked]="selection.isSelected(row)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="path">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Path</th>
|
||||
<td mat-cell *matCellDef="let element"> {{getNameByUuidFromEndpoint(element.path)}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="user/group">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>User/Group</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div *ngIf="element.ace_type === 'user' else groupId">{{getNameByUuidFromEndpoint(element.user_id)}}</div>
|
||||
<ng-template #groupId>{{getNameByUuidFromEndpoint(element.group_id)}}</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="role">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Role</th>
|
||||
<td mat-cell *matCellDef="let element"> {{getNameByUuidFromEndpoint(element.role_id)}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="propagate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Propagate</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.propagate}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="allowed">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Allowed</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.allowed}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="created_at">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Created</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.created_at}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="updated_at">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Last update</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.updated_at}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="delete">
|
||||
<th mat-header-cell *matHeaderCellDef> </th>
|
||||
<td mat-cell *matCellDef="let element"><button mat-button (click)="onDelete(element)"><mat-icon>delete</mat-icon></button></td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
|
||||
</table>
|
||||
<mat-paginator #acesPaginator="matPaginator"
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
aria-label="Select page">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<div>
|
||||
<mat-spinner class="loader"></mat-spinner>
|
||||
</div>
|
||||
</ng-template>
|
@ -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;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AclManagementComponent } from './acl-management.component';
|
||||
|
||||
describe('AclManagementComponent', () => {
|
||||
let component: AclManagementComponent;
|
||||
let fixture: ComponentFixture<AclManagementComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AclManagementComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AclManagementComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
176
src/app/components/acl-management/acl-management.component.ts
Normal file
176
src/app/components/acl-management/acl-management.component.ts
Normal file
@ -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/acl-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/acl-management/delete-ace-dialog/delete-ace-dialog.component";
|
||||
import {User} from "@models/users/user";
|
||||
import {Endpoint} from "@models/api/endpoint";
|
||||
|
||||
@Component({
|
||||
selector: 'app-acl-management',
|
||||
templateUrl: './acl-management.component.html',
|
||||
styleUrls: ['./acl-management.component.scss']
|
||||
})
|
||||
export class AclManagementComponent implements OnInit {
|
||||
|
||||
|
||||
@ViewChildren('acesPaginator') acesPaginator: QueryList<MatPaginator>;
|
||||
@ViewChildren('acesSort') acesSort: QueryList<MatSort>;
|
||||
controller: Controller;
|
||||
public displayedColumns = ['select', 'path', 'user/group', 'role', 'propagate', 'allowed', 'updated_at', 'delete'];
|
||||
selection = new SelectionModel<ACE>(true, []);
|
||||
aces: ACE[];
|
||||
dataSource = new MatTableDataSource<ACE>();
|
||||
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 <MatPaginator>) =>
|
||||
{
|
||||
this.dataSource.paginator = comps.first;
|
||||
});
|
||||
|
||||
this.acesSort.changes.subscribe((comps: QueryList<MatSort>) => {
|
||||
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 ''
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import {EndpointTreeAdapter} from "@components/acl-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);
|
||||
})
|
||||
})
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
<h1 mat-dialog-title>Create new ACE</h1>
|
||||
<form [formGroup]="addAceForm" class="input-field d-flex" style="height: 380px">
|
||||
<div style="width: 75%; float: left; overflow-y: auto" class="d-inline-block">
|
||||
<cdk-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<!-- This is the tree node template for leaf nodes -->
|
||||
<cdk-nested-tree-node *cdkTreeNodeDef="let node" class="example-tree-node">
|
||||
<!-- use a disabled button to provide padding for tree leaf -->
|
||||
<div [class.selected]="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint">
|
||||
<button mat-icon-button disabled></button>
|
||||
{{node.name}}
|
||||
<button mat-icon-button (click)="endpointSelection(node)">
|
||||
<div *ngIf="!selectedEndpoint || node.endpoint !== selectedEndpoint.endpoint">
|
||||
<mat-icon class="align-middle" style="color: #696969">panorama_fish_eye</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint" >
|
||||
<mat-icon class="align-middle">check_circle</mat-icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-nested-tree-node>
|
||||
<!-- This is the tree node template for expandable nodes -->
|
||||
<cdk-nested-tree-node *cdkTreeNodeDef="let node; when: hasChild" class="example-tree-node" >
|
||||
<div [class.selected]="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint">
|
||||
<button mat-icon-button [attr.aria-label]="'Toggle ' + node.name" cdkTreeNodeToggle>
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
{{node.name}}
|
||||
<button mat-icon-button (click)="endpointSelection(node)">
|
||||
<div *ngIf="!selectedEndpoint || node.endpoint !== selectedEndpoint.endpoint">
|
||||
<mat-icon class="align-middle" style="color: #696969">panorama_fish_eye</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="selectedEndpoint && node.endpoint === selectedEndpoint.endpoint">
|
||||
<mat-icon class="align-middle">check_circle</mat-icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div [class.example-tree-invisible]="!treeControl.isExpanded(node)">
|
||||
<ng-container cdkTreeNodeOutlet></ng-container>
|
||||
</div>
|
||||
</cdk-nested-tree-node>
|
||||
</cdk-tree>
|
||||
</div>
|
||||
<mat-divider [vertical]="true" style="height: auto;"></mat-divider>
|
||||
|
||||
<div class="form-div d-inline-block h-100">
|
||||
<div>
|
||||
<div class="typeSelect">
|
||||
<mat-select placeholder="User/Group" formControlName="type">
|
||||
<mat-option *ngFor="let t of types" [value]="t">{{t.charAt(0).toUpperCase() + t.slice(1)}}</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
|
||||
<app-autocomplete *ngIf="form.type.value === 'user'"
|
||||
[data]="users"
|
||||
[eltType]="'Users'"
|
||||
[displayFn]="displayFnUser"
|
||||
[filterFn]="_filterUser"
|
||||
(onSelection)="userSelection($event)">
|
||||
</app-autocomplete>
|
||||
|
||||
<app-autocomplete *ngIf="form.type.value === 'group'"
|
||||
[data]="groups"
|
||||
[eltType]="'Groups'"
|
||||
[displayFn]="displayFn"
|
||||
[filterFn]="_filter"
|
||||
(onSelection)="groupSelection($event)">
|
||||
</app-autocomplete>
|
||||
|
||||
<app-autocomplete
|
||||
[data]="roles"
|
||||
[eltType]="'Roles'"
|
||||
[displayFn]="displayFn"
|
||||
[filterFn]="_filter"
|
||||
(onSelection)="roleSelection($event)">
|
||||
</app-autocomplete>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-inline-block" style="float:left;">
|
||||
<mat-checkbox formControlName="propagate" >Propagate</mat-checkbox>
|
||||
</div>
|
||||
<mat-divider [vertical]="true" style="height: auto;"></mat-divider>
|
||||
<div class="d-inline-block" style="float: right">
|
||||
<button *ngIf="allowed" class="allow "
|
||||
mat-button
|
||||
(click)="changeAllowed()">ALLOWED
|
||||
</button>
|
||||
<button *ngIf="!allowed" class="deny"
|
||||
mat-button
|
||||
(click)="changeAllowed()">DENY
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions class="button-div">
|
||||
<button mat-button (click)="onCancelClick()" color="accent">Cancel</button>
|
||||
<button mat-button (click)="onAddClick()" tabindex="2" mat-raised-button color="primary"
|
||||
[disabled]="!addAceForm.valid">
|
||||
Add ACE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
@ -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
|
||||
}
|
@ -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<AddAceDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AddAceDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddAceDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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/acl-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<EndpointNode>(node => node.children);
|
||||
treeDataSource: ArrayDataSource<EndpointNode> ;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<AddAceDialogComponent>,
|
||||
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;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<mat-form-field class="input-field">
|
||||
<mat-label>{{eltType}}</mat-label>
|
||||
<input type="text"
|
||||
matInput
|
||||
[matAutocomplete]="auto"
|
||||
[formControl]="autocompleteControl">
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)='onSelection.emit($event.option.value)'>
|
||||
<mat-option *ngFor="let elt of filteredData | async" [value]="elt" >
|
||||
{{displayFn(elt)}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
@ -0,0 +1,3 @@
|
||||
.input-field {
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AutocompleteComponent } from './autocomplete.component';
|
||||
|
||||
describe('AutocompleteComponent', () => {
|
||||
let component: AutocompleteComponent<any>;
|
||||
let fixture: ComponentFixture<AutocompleteComponent<any>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AutocompleteComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AutocompleteComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<T> implements OnChanges {
|
||||
|
||||
@Input() data: T[];
|
||||
filteredData: Observable<T[]>;
|
||||
typeName: string
|
||||
autocompleteControl = new UntypedFormControl();
|
||||
|
||||
@Input() eltType: string
|
||||
@Input() displayFn: (value: T) => string
|
||||
@Input() filterFn: (value: string, data: T[]) => T[]
|
||||
@Output() onSelection: EventEmitter<T> = new EventEmitter<T>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.filteredData = this.autocompleteControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => this.filterFn(value, this.data))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<h1 mat-dialog-title>Are you sure you want to delete the following ACEs ?</h1>
|
||||
<ul>
|
||||
<li *ngFor="let ace of data.aces">{{ ace.path }} </li>
|
||||
</ul>
|
||||
<div mat-dialog-actions class="button-div">
|
||||
<button mat-button (click)="onCancel()" color="accent">No, cancel</button>
|
||||
<button mat-button (click)="onDelete()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||
Yes, delete!
|
||||
</button>
|
||||
</div>
|
@ -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<DeleteAceDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DeleteAceDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DeleteAceDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<DeleteAceDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { aces: ACE[] }) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
[routerLink]="['/controller', controller.id, 'management', 'groups']">
|
||||
<mat-icon aria-label="Back to group management">keyboard_arrow_left</mat-icon>
|
||||
</a>
|
||||
<h1 class="col">Groups {{group.name}} details</h1>
|
||||
<h1 class="col">Group {{group.name}}</h1>
|
||||
</div>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Details" class="details">
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox [checked]="group.is_builtin" disabled>Is build in</mat-checkbox>
|
||||
<mat-checkbox [checked]="group.is_builtin" disabled>Is built-in</mat-checkbox>
|
||||
</div>
|
||||
<div mat-dialog-actions class="button-div">
|
||||
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary"
|
||||
@ -56,15 +56,34 @@
|
||||
[pageSizeOptions]="[5, 20, 50, 100]"></mat-paginator>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Roles">
|
||||
<div><button mat-button (click)="openAddRoleDialog()" ><mat-icon>group_add</mat-icon></button></div>
|
||||
<div *ngFor="let role of roles" class="roles">
|
||||
<div>{{role.name}}</div>
|
||||
<div>
|
||||
<button mat-button (click)="openRemoveRoleDialog(role)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-tab label="ACEs">
|
||||
<div class="default-content">
|
||||
<table mat-table [dataSource]="aceDatasource" class="mat-elevation-z8">
|
||||
|
||||
<ng-container matColumnDef="endpoint">
|
||||
<th mat-header-cell *matHeaderCellDef> Endpoint </th>
|
||||
<td mat-cell *matCellDef="let element">{{element.endpoint_name}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="role">
|
||||
<th mat-header-cell *matHeaderCellDef > Role </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.role_name}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="propagate">
|
||||
<th mat-header-cell *matHeaderCellDef > Propagate </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.propagate}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="allowed">
|
||||
<th mat-header-cell *matHeaderCellDef> Allowed </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.allowed}} </td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="aceDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: aceDisplayedColumns;"></tr>
|
||||
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
@ -1,3 +1,7 @@
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
@ -22,8 +22,14 @@ import {RemoveToGroupDialogComponent} from "@components/group-details/remove-to-
|
||||
import {GroupService} from "@services/group.service";
|
||||
import {ToasterService} from "@services/toaster.service";
|
||||
import {PageEvent} from "@angular/material/paginator";
|
||||
import {ACE, ACEDetailed, AceType} from "@models/api/ACE";
|
||||
import {UserService} from "@services/user.service";
|
||||
import {RoleService} from "@services/role.service";
|
||||
import {Role} from "@models/api/role";
|
||||
import {AddRoleToGroupComponent} from "@components/group-details/add-role-to-group/add-role-to-group.component";
|
||||
import {AclService} from "@services/acl.service";
|
||||
import {Endpoint} from "@models/api/endpoint";
|
||||
import {interval} from "rxjs";
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
|
||||
@Component({
|
||||
selector: 'app-group-details',
|
||||
@ -37,28 +43,43 @@ export class GroupDetailsComponent implements OnInit {
|
||||
editGroupForm: UntypedFormGroup;
|
||||
pageEvent: PageEvent | undefined;
|
||||
searchMembers: string;
|
||||
roles: Role[];
|
||||
aces: ACE[];
|
||||
aceDatasource = new MatTableDataSource<ACEDetailed>();
|
||||
public aceDisplayedColumns = ['endpoint', 'role', 'propagate', 'allowed'];
|
||||
|
||||
constructor(private route: ActivatedRoute,
|
||||
private dialog: MatDialog,
|
||||
private groupService: GroupService,
|
||||
private toastService: ToasterService) {
|
||||
private toastService: ToasterService,
|
||||
private aclService: AclService,
|
||||
private roleService: RoleService) {
|
||||
|
||||
this.editGroupForm = new UntypedFormGroup({
|
||||
groupname: new UntypedFormControl(''),
|
||||
});
|
||||
|
||||
this.route.data.subscribe((d: { controller: Controller; group: Group, members: User[], roles: Role[] }) => {
|
||||
this.route.data.subscribe((d: { controller: Controller; group: Group, members: User[], aces: ACE[] }) => {
|
||||
|
||||
this.controller = d.controller;
|
||||
this.group = d.group;
|
||||
this.roles = d.roles;
|
||||
this.aces = d.aces;
|
||||
this.members = d.members.sort((a: User, b: User) => a.username.toLowerCase().localeCompare(b.username.toLowerCase()));
|
||||
this.editGroupForm.setValue({groupname: this.group.name});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.roleService.get(this.controller).subscribe((roles: Role[]) => {
|
||||
this.aclService.getEndpoints(this.controller).subscribe((endps: Endpoint[]) => {
|
||||
this.aceDatasource.data = this.aces.map((ace: ACE) => {
|
||||
const endpoint = endps.filter((endp: Endpoint) => endp.endpoint === ace.path)[0]
|
||||
const role = roles.filter((r: Role) => r.role_id === ace.role_id)[0]
|
||||
return {...ace, endpoint_name: endpoint.name, role_name: role.name}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -72,18 +93,6 @@ export class GroupDetailsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
openAddRoleDialog() {
|
||||
this.dialog
|
||||
.open<AddRoleToGroupComponent>(AddRoleToGroupComponent,
|
||||
{
|
||||
width: '700px', height: '500px',
|
||||
data: {controller: this.controller, group: this.group}
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe(() => {
|
||||
this.reloadRoles();
|
||||
});
|
||||
}
|
||||
openAddUserDialog() {
|
||||
this.dialog
|
||||
.open<AddUserToGroupDialogComponent>(AddUserToGroupDialogComponent,
|
||||
@ -117,24 +126,6 @@ export class GroupDetailsComponent implements OnInit {
|
||||
}
|
||||
|
||||
|
||||
openRemoveRoleDialog(role: Role) {
|
||||
this.dialog.open<RemoveToGroupDialogComponent>(RemoveToGroupDialogComponent,
|
||||
{width: '500px', height: '200px', data: {name: role.name}})
|
||||
.afterClosed()
|
||||
.subscribe((confirm: string) => {
|
||||
if (confirm) {
|
||||
this.groupService.removeRole(this.controller, this.group, role)
|
||||
.subscribe(() => {
|
||||
this.toastService.success(`Role ${role.name} was removed`);
|
||||
this.reloadRoles();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(`Error while removing role ${role.name} from ${this.group.name}`);
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reloadMembers() {
|
||||
this.groupService.getGroupMember(this.controller, this.group.user_group_id)
|
||||
@ -143,10 +134,4 @@ export class GroupDetailsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
reloadRoles() {
|
||||
this.groupService.getGroupRole(this.controller, this.group.user_group_id)
|
||||
.subscribe((roles: Role[]) => {
|
||||
this.roles = roles;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="content" *ngIf="isReady; else loading">
|
||||
<div class="default-header">
|
||||
<div class="row">
|
||||
<h1 class="col">Groups management</h1>
|
||||
<h1 class="col">Groups</h1>
|
||||
<button class="col" mat-raised-button color="primary" (click)="onDelete(selection.selected)" class="add-group-button" [disabled]="selection.selected.length == 0">
|
||||
Delete selected groups
|
||||
</button>
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
<form>
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Search by name" [(ngModel)]="searchText" [ngModelOptions]="{ standalone: true }" />
|
||||
<input matInput placeholder="Search group by name" [(ngModel)]="searchText" [ngModelOptions]="{ standalone: true }" />
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
|
||||
|
||||
<ng-container matColumnDef="is_builtin">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> is build in</th>
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> is built-in</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.is_builtin}} </td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -23,7 +23,7 @@ import {ControllerService} from "@services/controller.service";
|
||||
export class ManagementComponent implements OnInit {
|
||||
|
||||
controller: Controller;
|
||||
links = ['users', 'groups', 'roles', 'permissions'];
|
||||
links = ['users', 'groups', 'roles', 'pools', 'ACL'];
|
||||
activeLink: string = this.links[0];
|
||||
|
||||
constructor(
|
||||
|
@ -1,6 +0,0 @@
|
||||
<button [ngClass]="{allow: action === 'ALLOW', deny: action === 'DENY'}"
|
||||
mat-button
|
||||
[disabled]="disabled"
|
||||
(click)="change()">
|
||||
{{action}}
|
||||
</button>
|
@ -1,8 +0,0 @@
|
||||
.allow {
|
||||
background-color: green;
|
||||
border-radius: unset !important;
|
||||
}
|
||||
|
||||
.deny {
|
||||
background-color: darkred;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {PermissionActions} from "@models/api/permission";
|
||||
|
||||
@Component({
|
||||
selector: 'app-action-button',
|
||||
templateUrl: './action-button.component.html',
|
||||
styleUrls: ['./action-button.component.scss']
|
||||
})
|
||||
export class ActionButtonComponent implements OnInit {
|
||||
|
||||
readonly DENY = 'DENY';
|
||||
readonly ALLOW = 'ALLOW';
|
||||
@Input() action: PermissionActions;
|
||||
@Input() disabled = true;
|
||||
@Output() update = new EventEmitter<PermissionActions>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
change() {
|
||||
this.action === PermissionActions.DENY ? this.action = PermissionActions.ALLOW : this.action = PermissionActions.DENY;
|
||||
this.update.emit(this.action);
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
<div class="box-border">
|
||||
<div *ngIf="edit; else add">
|
||||
<div class="edit-mode">
|
||||
<div class="information-box">
|
||||
<div>
|
||||
<app-path-auto-complete
|
||||
[controller]="controller"
|
||||
(update)="permission.path = $event"></app-path-auto-complete>
|
||||
</div>
|
||||
<div class="methods">
|
||||
<app-action-button
|
||||
[disabled]="false"
|
||||
[action]="permission.action"></app-action-button>
|
||||
<div *ngFor="let method of apiInformation.getMethods(permission.path) | async">
|
||||
<app-method-button
|
||||
[name]="method"
|
||||
[disabled]="false"
|
||||
(update)="updateMethod($event)"></app-method-button>
|
||||
</div>
|
||||
<div class="description">
|
||||
<mat-form-field>
|
||||
<input
|
||||
[(ngModel)]="permission.description"
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Description"/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-box">
|
||||
<button mat-button (click)="reset()">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
<button mat-button (click)="save()">
|
||||
<mat-icon>done</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #add>
|
||||
<div class="not-edit">
|
||||
<button mat-button (click)="edit = true">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
@ -1,49 +0,0 @@
|
||||
.box-border {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.edit-mode {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.information-box {
|
||||
margin-left: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.information-box > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.methods {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.description > mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.not-edit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
/* tslint:disable:no-shadowed-variable */
|
||||
import {fakeAsync, TestBed, tick} from "@angular/core/testing";
|
||||
import {AddPermissionLineComponent} from "@components/permissions-management/add-permission-line/add-permission-line.component";
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {PermissionsService} from "@services/permissions.service";
|
||||
import {ToasterService} from "@services/toaster.service";
|
||||
import {Methods, Permission, PermissionActions} from "@models/api/permission";
|
||||
import {Controller} from "@models/controller";
|
||||
import {Observable, of, throwError} from "rxjs";
|
||||
import {HttpErrorResponse} from "@angular/common/http";
|
||||
|
||||
class MockApiInformationService {
|
||||
|
||||
}
|
||||
|
||||
class MockPermissionService {
|
||||
}
|
||||
|
||||
class MockToasterService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
describe('AddPermissionLineComponent', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AddPermissionLineComponent,
|
||||
{provide: ApiInformationService, useClass: MockApiInformationService},
|
||||
{provide: PermissionsService, useClass: MockPermissionService},
|
||||
{provide: ToasterService, useClass: MockToasterService}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('Should add GET method to method list', () => {
|
||||
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||
comp.updateMethod({name: Methods.GET, enable: true});
|
||||
expect(comp.permission.methods).toContain(Methods.GET);
|
||||
});
|
||||
|
||||
it('Should remove GET Method from list', () => {
|
||||
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||
comp.permission.methods = [Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE];
|
||||
comp.updateMethod({name: Methods.GET, enable: false});
|
||||
|
||||
expect(comp.permission.methods).not.toContain(Methods.GET);
|
||||
});
|
||||
|
||||
it('Should not add same GET method a second time', () => {
|
||||
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||
comp.permission.methods = [Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE];
|
||||
comp.updateMethod({name: Methods.GET, enable: true});
|
||||
|
||||
expect(comp.permission.methods).toEqual([Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE]);
|
||||
});
|
||||
|
||||
it('Should reset permission values', () => {
|
||||
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||
comp.permission.methods = [Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE];
|
||||
comp.permission.path = "/test/path";
|
||||
comp.permission.action = PermissionActions.DENY;
|
||||
comp.permission.description = "john doe is here";
|
||||
|
||||
comp.reset();
|
||||
const p = comp.permission;
|
||||
|
||||
expect(p.methods).toEqual([]);
|
||||
expect(p.action).toEqual(PermissionActions.ALLOW);
|
||||
expect(p.description).toEqual('');
|
||||
});
|
||||
|
||||
it('Should save permission with success', fakeAsync(() => {
|
||||
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||
const permissionService = TestBed.inject(PermissionsService);
|
||||
const toasterService = TestBed.inject(ToasterService);
|
||||
comp.permission.methods = [Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE];
|
||||
comp.permission.path = "/test/path";
|
||||
comp.permission.action = PermissionActions.DENY;
|
||||
comp.permission.description = "john doe is here";
|
||||
|
||||
permissionService.add = (controller: Controller, permission: Permission): Observable<Permission> => {
|
||||
return of(permission);
|
||||
};
|
||||
|
||||
let message: string;
|
||||
|
||||
toasterService.success = (m: string) => {
|
||||
message = m;
|
||||
};
|
||||
|
||||
comp.save();
|
||||
const p = comp.permission;
|
||||
|
||||
tick();
|
||||
expect(message).toBeTruthy();
|
||||
expect(p.methods).toEqual([]);
|
||||
expect(p.action).toEqual(PermissionActions.ALLOW);
|
||||
expect(p.description).toEqual('');
|
||||
|
||||
}));
|
||||
|
||||
it('Should throw error on rejected permission', fakeAsync(() => {
|
||||
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||
const permissionService = TestBed.inject(PermissionsService);
|
||||
const toasterService = TestBed.inject(ToasterService);
|
||||
comp.permission.methods = [Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE];
|
||||
comp.permission.path = "/test/path";
|
||||
comp.permission.action = PermissionActions.DENY;
|
||||
comp.permission.description = "john doe is here";
|
||||
|
||||
let errorMessage: string;
|
||||
|
||||
permissionService.add = (controller: Controller, permission: Permission): Observable<Permission> => {
|
||||
const error = new HttpErrorResponse({
|
||||
error: new Error("An error occur"),
|
||||
headers: undefined,
|
||||
status: 500,
|
||||
statusText: 'error from controller'
|
||||
});
|
||||
return throwError(error);
|
||||
};
|
||||
|
||||
toasterService.error = (message: string) => {
|
||||
errorMessage = message;
|
||||
};
|
||||
|
||||
comp.save();
|
||||
tick();
|
||||
expect(errorMessage).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {Controller} from "@models/controller";
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {Methods, Permission, PermissionActions} from "@models/api/permission";
|
||||
import {PermissionsService} from "@services/permissions.service";
|
||||
import {ToasterService} from "@services/toaster.service";
|
||||
import {HttpErrorResponse} from "@angular/common/http";
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-permission-line',
|
||||
templateUrl: './add-permission-line.component.html',
|
||||
styleUrls: ['./add-permission-line.component.scss']
|
||||
})
|
||||
export class AddPermissionLineComponent implements OnInit {
|
||||
|
||||
@Input() controller: Controller;
|
||||
@Output() addPermissionEvent = new EventEmitter<void>();
|
||||
permission: Permission = {
|
||||
action: PermissionActions.ALLOW,
|
||||
description: "",
|
||||
methods: [],
|
||||
path: "/"
|
||||
};
|
||||
edit = false;
|
||||
|
||||
constructor(public apiInformation: ApiInformationService,
|
||||
private permissionService: PermissionsService,
|
||||
private toasterService: ToasterService) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
|
||||
updateMethod(data: { name: Methods; enable: boolean }) {
|
||||
const set = new Set(this.permission.methods);
|
||||
if (data.enable) {
|
||||
set.add(data.name);
|
||||
} else {
|
||||
set.delete(data.name);
|
||||
}
|
||||
|
||||
this.permission.methods = Array.from(set);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.permission = {
|
||||
action: PermissionActions.ALLOW,
|
||||
description: "",
|
||||
methods: [],
|
||||
path: "/",
|
||||
};
|
||||
|
||||
this.edit = false;
|
||||
}
|
||||
|
||||
save() {
|
||||
this.permissionService.add(this.controller, this.permission)
|
||||
.subscribe(() => {
|
||||
this.toasterService.success(`permission was created`);
|
||||
this.reset();
|
||||
}, (error: HttpErrorResponse) => {
|
||||
this.toasterService.error(`
|
||||
${error.message}
|
||||
${error.error.message}`);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
import {PermissionPath} from "@components/permissions-management/add-permission-line/path-auto-complete/PermissionPath";
|
||||
import {SubPath} from "@components/permissions-management/add-permission-line/path-auto-complete/SubPath";
|
||||
|
||||
describe('PermissionPath', () => {
|
||||
|
||||
it('Should add subPath to path', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
|
||||
expect(path.getPath()).toEqual(['projects', '1111-2222-3333-4444']);
|
||||
});
|
||||
|
||||
it('Should return display path', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
|
||||
expect(path.getDisplayPath()).toEqual(['projects', 'my project']);
|
||||
});
|
||||
|
||||
it('Should remove last element', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
path.add(new SubPath('nodes', 'nodes'));
|
||||
path.add(new SubPath('6666-7777-8888-9999', 'myFirstNode', 'node_id'));
|
||||
|
||||
path.removeLast();
|
||||
expect(path.getPath()).toEqual(['projects', '1111-2222-3333-4444', 'nodes']);
|
||||
});
|
||||
|
||||
it('Should return path variables', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
path.add(new SubPath('nodes', 'nodes'));
|
||||
path.add(new SubPath('6666-7777-8888-9999', 'myFirstNode', 'node_id'));
|
||||
|
||||
expect(path.getVariables())
|
||||
.toEqual([{key: 'project_id', value: '1111-2222-3333-4444'}, { key: 'node_id', value: '6666-7777-8888-9999'}]);
|
||||
});
|
||||
|
||||
|
||||
it('Should return true if subPath contain *', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
path.add(new SubPath('nodes', 'nodes'));
|
||||
path.add(new SubPath('*', 'myFirstNode', 'node_id'));
|
||||
|
||||
expect(path.containStar()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should return false if subPath does not contain *', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
path.add(new SubPath('nodes', 'nodes'));
|
||||
path.add(new SubPath('6666-7777-8888-999', 'myFirstNode', 'node_id'));
|
||||
|
||||
expect(path.containStar()).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
it('Should return true if path is empty', () => {
|
||||
const path = new PermissionPath();
|
||||
expect(path.isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('Should return false if path is not empty', () => {
|
||||
const path = new PermissionPath();
|
||||
path.add(new SubPath('projects', 'projects', undefined));
|
||||
path.add(new SubPath('1111-2222-3333-4444', 'my project', 'project_id'));
|
||||
path.add(new SubPath('nodes', 'nodes'));
|
||||
path.add(new SubPath('6666-7777-8888-999', 'myFirstNode', 'node_id'));
|
||||
|
||||
expect(path.isEmpty()).toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* 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 {SubPath} from "./SubPath";
|
||||
|
||||
export class PermissionPath {
|
||||
private subPath: SubPath[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
add(subPath: SubPath) {
|
||||
this.subPath.push(subPath);
|
||||
}
|
||||
|
||||
getDisplayPath() {
|
||||
return this.subPath
|
||||
.map((subPath) => subPath.displayValue);
|
||||
}
|
||||
|
||||
removeLast() {
|
||||
this.subPath.pop();
|
||||
}
|
||||
|
||||
getPath() {
|
||||
return this.subPath.map((subPath) => subPath.value);
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this.subPath.length === 0;
|
||||
}
|
||||
|
||||
getVariables(): { key: string; value: string }[] {
|
||||
return this.subPath
|
||||
.filter((path) => path.key)
|
||||
.map((path) => {
|
||||
return {key: path.key, value: path.value};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
containStar() {
|
||||
return this.subPath
|
||||
.map(subPath => subPath.value === '*')
|
||||
.reduce((previous, next) => previous || next, false);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import {FilterCompletePipe} from "@components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe";
|
||||
import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject";
|
||||
|
||||
describe('FilterCompletePipe', () => {
|
||||
it('should remove items which not match searchText', function () {
|
||||
const filter = new FilterCompletePipe();
|
||||
|
||||
const items: IGenericApiObject[] = [
|
||||
{id: 'b2afe0da-b83e-42a8-bcb6-e46ca1bd1747', name: 'test project 1'},
|
||||
{id: '698d35c1-9fd0-4b89-86dc-336a958b1f70', name: 'test project 2'},
|
||||
{id: '4bbd57e6-bf99-4387-8948-7e7d8e96de9b', name: 'test project 3'},
|
||||
{id: '29e9ddb6-1ba0-422d-b767-92592821f011', name: 'test project 4'},
|
||||
{id: '5a522134-0bfd-4864-b8b3-520bcecd4fc9', name: 'test project 5'},
|
||||
{id: '7e27f67a-2b63-4d00-936b-e3d8c7e2b751', name: 'test project 6'},
|
||||
];
|
||||
|
||||
expect(filter.transform(items, 'test project 6'))
|
||||
.toEqual([{id: '7e27f67a-2b63-4d00-936b-e3d8c7e2b751', name: 'test project 6'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should return entire list if searchText is empty', function () {
|
||||
const filter = new FilterCompletePipe();
|
||||
|
||||
const items: IGenericApiObject[] = [
|
||||
{id: 'b2afe0da-b83e-42a8-bcb6-e46ca1bd1747', name: 'test project 1'},
|
||||
{id: '698d35c1-9fd0-4b89-86dc-336a958b1f70', name: 'test project 2'},
|
||||
{id: '4bbd57e6-bf99-4387-8948-7e7d8e96de9b', name: 'test project 3'},
|
||||
{id: '29e9ddb6-1ba0-422d-b767-92592821f011', name: 'test project 4'},
|
||||
{id: '5a522134-0bfd-4864-b8b3-520bcecd4fc9', name: 'test project 5'},
|
||||
{id: '7e27f67a-2b63-4d00-936b-e3d8c7e2b751', name: 'test project 6'},
|
||||
];
|
||||
|
||||
expect(filter.transform(items, '')).toEqual(items);
|
||||
expect(filter.transform(items, undefined)).toEqual(items);
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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 { Pipe, PipeTransform } from '@angular/core';
|
||||
import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject";
|
||||
|
||||
/**
|
||||
* Pipe to filter autocomplete proposals
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'filterComplete'
|
||||
})
|
||||
export class FilterCompletePipe implements PipeTransform {
|
||||
|
||||
transform(value: IGenericApiObject[], searchText: string): IGenericApiObject[] {
|
||||
if (!searchText || searchText === '') { return value; }
|
||||
|
||||
return value.filter((v) => {
|
||||
return v.name.includes(searchText) || v.id.includes(searchText);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<div class="path">
|
||||
<div>Path: /</div>
|
||||
<div *ngFor="let p of path.getDisplayPath()">{{p}}/</div>
|
||||
<div class="path-edit-line">
|
||||
<div>
|
||||
<div *ngIf="mode === 'SELECT'">
|
||||
<mat-select (valueChange)="valueChanged($event)" class="edit-area">
|
||||
<mat-option *ngFor="let value of values" value="{{value}}">{{value}}</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<div *ngIf="mode === 'COMPLETE'">
|
||||
<input matInput
|
||||
autofocus
|
||||
class="complete edit-area"
|
||||
aria-label="find"
|
||||
[(ngModel)]="completeField"
|
||||
[matAutocomplete]="auto">
|
||||
<mat-autocomplete #auto="matAutocomplete">
|
||||
<mat-option [value]="'*'">*</mat-option>
|
||||
<mat-option *ngFor="let data of completeData.data | filterComplete: completeField"
|
||||
[value]="data.name">
|
||||
<span>{{data.name}}</span>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-button">
|
||||
<mat-icon (click)="removePrevious()" *ngIf="!path.isEmpty()">cancel</mat-icon>
|
||||
<mat-icon (click)="getNext()" *ngIf="!this.mode">add_circle_outline</mat-icon>
|
||||
<mat-icon
|
||||
matTooltip="validate data"
|
||||
(click)="validComplete()"
|
||||
*ngIf="this.mode === 'COMPLETE'">check_circle
|
||||
</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,22 +0,0 @@
|
||||
.path {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
mat-select {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.edit-area {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.command-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.path-edit-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
/*
|
||||
* 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {Controller} from "@models/controller";
|
||||
import {PermissionPath} from "@components/permissions-management/add-permission-line/path-auto-complete/PermissionPath";
|
||||
import {SubPath} from "@components/permissions-management/add-permission-line/path-auto-complete/SubPath";
|
||||
import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject";
|
||||
|
||||
@Component({
|
||||
selector: 'app-path-auto-complete',
|
||||
templateUrl: './path-auto-complete.component.html',
|
||||
styleUrls: ['./path-auto-complete.component.scss']
|
||||
})
|
||||
export class PathAutoCompleteComponent implements OnInit {
|
||||
|
||||
|
||||
@Output() update = new EventEmitter<string>();
|
||||
@Input() controller: Controller;
|
||||
path: PermissionPath = new PermissionPath();
|
||||
values: string[] = [];
|
||||
private completeData: { data: IGenericApiObject[]; key: string };
|
||||
public mode: 'SELECT' | 'COMPLETE' | undefined;
|
||||
completeField: string;
|
||||
|
||||
constructor(private apiInformationService: ApiInformationService) {
|
||||
|
||||
}
|
||||
|
||||
updatePath(name: string, value: string, key?: string) {
|
||||
this.path.add(new SubPath(name, value, key));
|
||||
this.update.emit('/' + this.path.getPath().join("/"));
|
||||
}
|
||||
|
||||
popPath() {
|
||||
this.path.removeLast();
|
||||
this.update.emit('/' + this.path.getPath().join("/"));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
getNext() {
|
||||
this.apiInformationService
|
||||
.getPathNextElement(this.path.getPath())
|
||||
.subscribe((next: string[]) => {
|
||||
if (this.path.containStar()) {
|
||||
next = next.filter(item => !item.match(this.apiInformationService.bracketIdRegex));
|
||||
}
|
||||
this.values = next;
|
||||
this.mode = 'SELECT';
|
||||
});
|
||||
}
|
||||
|
||||
removePrevious() {
|
||||
if (this.mode) {
|
||||
return this.mode = undefined;
|
||||
}
|
||||
if (!this.path.isEmpty()) {
|
||||
return this.popPath();
|
||||
}
|
||||
}
|
||||
|
||||
valueChanged(value: string) {
|
||||
if (value.match(this.apiInformationService.bracketIdRegex) && !this.path.containStar()) {
|
||||
this.apiInformationService.getListByObjectId(this.controller, value, undefined, this.path.getVariables())
|
||||
.subscribe((data) => {
|
||||
this.mode = 'COMPLETE';
|
||||
this.completeData = {data, key: value};
|
||||
});
|
||||
|
||||
} else {
|
||||
this.updatePath(value, value);
|
||||
this.mode = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
validComplete() {
|
||||
if (this.completeField === '*') {
|
||||
this.updatePath('*', '*');
|
||||
} else {
|
||||
const data = this.completeData.data.find((d) => this.completeField === d.name);
|
||||
this.updatePath(data.id, data.name, this.completeData.key);
|
||||
}
|
||||
this.mode = undefined;
|
||||
this.completeField = undefined;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<div class="description">
|
||||
<div>confirm deleting permission:</div>
|
||||
<div>{{data.permission_id}}</div>
|
||||
<div>{{data.path}}</div>
|
||||
<div>{{data.methods.join(',')}}</div>
|
||||
<div>{{data.action}}</div>
|
||||
<div>{{data.description}}</div>
|
||||
</div>
|
||||
<div class="button">
|
||||
<button mat-button mat-raised-button (click)="cancel()">No, cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" (click)="confirm()">Yes, remove</button>
|
||||
</div>
|
@ -1,18 +0,0 @@
|
||||
.description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.description > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import {async, fakeAsync, TestBed, tick} from "@angular/core/testing";
|
||||
import {DisplayPathPipe} from "@components/permissions-management/display-path.pipe";
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {Controller} from "@models/controller";
|
||||
import {Observable, of} from "rxjs";
|
||||
import {IExtraParams} from "@services/ApiInformation/IExtraParams";
|
||||
import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject";
|
||||
|
||||
class MockApiInformationService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
describe('DisplayPathPipe', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DisplayPathPipe,
|
||||
{provide: ApiInformationService, useClass: MockApiInformationService}]
|
||||
});
|
||||
}));
|
||||
|
||||
it('Should display human readable path', fakeAsync(() => {
|
||||
const comp = TestBed.inject(DisplayPathPipe);
|
||||
const apiService = TestBed.inject(ApiInformationService);
|
||||
|
||||
apiService.getKeysForPath = (path: string): Observable<{ key: string; value: string }[]> => {
|
||||
return of([
|
||||
{key: 'project_id', value: '1111-2222-3333'},
|
||||
{key: 'node_id', value: '2222-2222-2222'}
|
||||
]);
|
||||
};
|
||||
|
||||
apiService
|
||||
.getListByObjectId = (controller: Controller, key: string, value?: string, extraParams?: IExtraParams[]): Observable<IGenericApiObject[]> => {
|
||||
if (key === 'project_id') {
|
||||
return of([{id: '1111-2222-3333', name: 'myProject'}]);
|
||||
}
|
||||
if (key === 'node_id') {
|
||||
return of([{id: '2222-2222-2222', name: 'node1'}]);
|
||||
}
|
||||
};
|
||||
|
||||
let result: string;
|
||||
|
||||
const controller = new Controller();
|
||||
comp
|
||||
.transform('/project/1111-2222-3333/nodes/2222-2222-2222', controller)
|
||||
.subscribe((res: string) => {
|
||||
result = res;
|
||||
});
|
||||
|
||||
tick();
|
||||
expect(result).toEqual('/project/myProject/nodes/node1');
|
||||
}));
|
||||
});
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* 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 {Pipe, PipeTransform} from '@angular/core';
|
||||
import {map, switchMap} from "rxjs/operators";
|
||||
import {forkJoin, Observable, of} from "rxjs";
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {Controller} from "@models/controller";
|
||||
|
||||
@Pipe({
|
||||
name: 'displayPath'
|
||||
})
|
||||
export class DisplayPathPipe implements PipeTransform {
|
||||
|
||||
constructor(private apiInformation: ApiInformationService) {
|
||||
}
|
||||
|
||||
transform(originalPath: string, controller: Controller): Observable<string> {
|
||||
if (!controller) {
|
||||
return of(originalPath);
|
||||
}
|
||||
return this.apiInformation
|
||||
.getKeysForPath(originalPath)
|
||||
.pipe(switchMap((values) => {
|
||||
if (values.length === 0) {
|
||||
return of([]);
|
||||
}
|
||||
const obs = values.map((k) => this.apiInformation.getListByObjectId(controller, k.key, k.value, values));
|
||||
return forkJoin(obs);
|
||||
}),
|
||||
map((values: { id: string; name: string }[][]) => {
|
||||
let displayPath = `${originalPath}`;
|
||||
values.forEach((value) => {
|
||||
if (value[0].id && value[0].name) {
|
||||
displayPath = displayPath.replace(value[0].id, value[0].name);
|
||||
} else {
|
||||
}
|
||||
|
||||
});
|
||||
return displayPath;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<button
|
||||
[disabled]="disabled"
|
||||
[ngClass]="{enable: enable, disabled: disabled}"
|
||||
(click)="change()"
|
||||
mat-button>
|
||||
{{name}}
|
||||
</button>
|
@ -1,10 +0,0 @@
|
||||
:host {
|
||||
padding: unset !important;
|
||||
}
|
||||
|
||||
.enable {
|
||||
color: green !important;
|
||||
}
|
||||
.disabled {
|
||||
color: dimgrey;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import {async, fakeAsync, TestBed} from "@angular/core/testing";
|
||||
import {MethodButtonComponent} from "@components/permissions-management/method-button/method-button.component";
|
||||
import {Methods} from "@models/api/permission";
|
||||
|
||||
describe('MethodButtonComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({declarations: [MethodButtonComponent]});
|
||||
}));
|
||||
|
||||
it('Should set text color to green when button is enable', fakeAsync(() => {
|
||||
const fixture = TestBed.createComponent(MethodButtonComponent);
|
||||
const component = fixture.componentInstance;
|
||||
const debugElement = fixture.debugElement;
|
||||
|
||||
component.enable = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(debugElement.nativeElement.querySelector('button').classList).toContain('enable');
|
||||
|
||||
}));
|
||||
|
||||
it('Should switch to enable on button click', (() => {
|
||||
const fixture = TestBed.createComponent(MethodButtonComponent);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.enable = false;
|
||||
component.change();
|
||||
|
||||
expect(component.enable).toEqual(true);
|
||||
|
||||
}));
|
||||
|
||||
|
||||
it('Should emit event enable on button click', (() => {
|
||||
const fixture = TestBed.createComponent(MethodButtonComponent);
|
||||
const component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
component.update.subscribe((data) => {
|
||||
expect(data.enable).toEqual(true);
|
||||
expect(data.name).toEqual(Methods.GET);
|
||||
});
|
||||
|
||||
component.name = Methods.GET;
|
||||
component.enable = false;
|
||||
component.change();
|
||||
|
||||
}));
|
||||
|
||||
|
||||
});
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {Methods} from "@models/api/permission";
|
||||
|
||||
@Component({
|
||||
selector: 'app-method-button',
|
||||
templateUrl: './method-button.component.html',
|
||||
styleUrls: ['./method-button.component.scss']
|
||||
})
|
||||
export class MethodButtonComponent implements OnInit {
|
||||
|
||||
@Input() enable = false;
|
||||
@Input() name: Methods;
|
||||
@Input() disabled = true;
|
||||
|
||||
@Output() update = new EventEmitter<{name: Methods; enable: boolean}>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
change() {
|
||||
this.enable = !this.enable;
|
||||
this.update.emit({name: this.name, enable: this.enable});
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
<div class="permission">
|
||||
<div class="action-button-bar">
|
||||
<div>
|
||||
<app-action-button
|
||||
[action]="permission.action"
|
||||
[disabled]="!isEditable"
|
||||
(update)="permission.action = $event"></app-action-button>
|
||||
</div>
|
||||
<div class="methods">
|
||||
<div *ngFor="let method of apiInformation.getMethods(permission.path) | async">
|
||||
<app-method-button
|
||||
[name]="method"
|
||||
[disabled]="!isEditable"
|
||||
[enable]="permission.methods.includes(method)"
|
||||
(update)="onMethodUpdate($event)"></app-method-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
[matTooltip]="permission.path | displayPath: controller | async"
|
||||
matTooltipClass="custom-tooltip">
|
||||
{{permission.path | displayPath: controller | async}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field class="permission-input" appearance="none">
|
||||
<input
|
||||
[(ngModel)]="permission.description"
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
[matTooltip]="permission.description"
|
||||
matTooltipClass="custom-tooltip"
|
||||
[readonly]="!isEditable"/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="button-bar">
|
||||
<div>
|
||||
<button mat-button matTooltip="Edit permission" (click)="isEditable = true" *ngIf="!isEditable">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-button matTooltip="Delete permission" (click)="onDelete()" *ngIf="!isEditable">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-button matTooltip="Save Changes" (click)="onSave()" *ngIf="isEditable" color="primary">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
</button>
|
||||
<button mat-button matTooltip="Cancel Changes" color="warn" (click)="onCancel()" *ngIf="isEditable">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,28 +0,0 @@
|
||||
.permission {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-bottom: solid 1px;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-button-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.methods {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.permission-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.button-bar > div > button {
|
||||
padding: unset !important;
|
||||
}
|
||||
|
@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {Methods, Permission} from "@models/api/permission";
|
||||
import {Controller} from '@models/controller';
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {PermissionsService} from "@services/permissions.service";
|
||||
import {ToasterService} from "@services/toaster.service";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {DeletePermissionDialogComponent} from "@components/permissions-management/delete-permission-dialog/delete-permission-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-permission-add-edit-line',
|
||||
templateUrl: './permission-edit-line.component.html',
|
||||
styleUrls: ['./permission-edit-line.component.scss']
|
||||
})
|
||||
export class PermissionEditLineComponent {
|
||||
@Input() permission: Permission;
|
||||
@Input() controller: Controller;
|
||||
|
||||
isEditable = false;
|
||||
@Output() update = new EventEmitter<void>();
|
||||
|
||||
constructor(public apiInformation: ApiInformationService,
|
||||
private permissionService: PermissionsService,
|
||||
private toasterService: ToasterService,
|
||||
private dialog: MatDialog) {
|
||||
}
|
||||
|
||||
|
||||
onDelete() {
|
||||
this.dialog.open<DeletePermissionDialogComponent>(DeletePermissionDialogComponent,
|
||||
{width: '700px', height: '500px', data: this.permission})
|
||||
.afterClosed()
|
||||
.subscribe((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
this.permissionService.delete(this.controller, this.permission.permission_id)
|
||||
.subscribe(() => {
|
||||
this.toasterService.success(`Permission was deleted`);
|
||||
this.update.emit();
|
||||
}, (e) => {
|
||||
this.toasterService.error(e);
|
||||
this.update.emit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onSave() {
|
||||
this.permissionService.update(this.controller, this.permission)
|
||||
.subscribe(() => {
|
||||
this.toasterService.success(`Permission was updated`);
|
||||
this.update.emit();
|
||||
}, (e) => {
|
||||
this.toasterService.error(e);
|
||||
this.update.emit();
|
||||
});
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.update.emit();
|
||||
}
|
||||
|
||||
|
||||
onMethodUpdate(event: { name: Methods; enable: boolean }) {
|
||||
const set = new Set(this.permission.methods);
|
||||
event.enable ? set.add(event.name) : set.delete(event.name);
|
||||
this.permission.methods = Array.from(set);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import {PermissionsFilterPipe} from './permissions-filter.pipe';
|
||||
import {Methods, Permission, PermissionActions} from "../../models/api/permission";
|
||||
|
||||
const testPermissions: Permission[] = [
|
||||
{
|
||||
methods: [Methods.GET, Methods.PUT],
|
||||
path: '/projects/projet-test',
|
||||
action: PermissionActions.ALLOW,
|
||||
description: 'description of permission 1',
|
||||
created_at: "2022-03-15T09:45:36.531Z",
|
||||
updated_at: "2022-03-15T09:45:36.531Z",
|
||||
permission_id: '1'
|
||||
},
|
||||
{
|
||||
methods: [Methods.GET, Methods.PUT],
|
||||
path: '/projects/projet-test/nodes',
|
||||
action: PermissionActions.ALLOW,
|
||||
description: 'permission on projet-test nodes',
|
||||
created_at: "2022-03-15T09:45:36.531Z",
|
||||
updated_at: "2022-03-15T09:45:36.531Z",
|
||||
permission_id: '2'
|
||||
},
|
||||
{
|
||||
methods: [Methods.GET, Methods.PUT],
|
||||
path: '/projects/projet-bidule',
|
||||
action: PermissionActions.ALLOW,
|
||||
description: 'permission on biduler project',
|
||||
created_at: "2022-03-15T09:45:36.531Z",
|
||||
updated_at: "2022-03-15T09:45:36.531Z",
|
||||
permission_id: '3'
|
||||
}
|
||||
]
|
||||
|
||||
describe('PermissionsFilterPipe', () => {
|
||||
const pipe = new PermissionsFilterPipe();
|
||||
|
||||
it('create an instance', () => {
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should return all test permissions', () => {
|
||||
const res = pipe.transform(testPermissions, '');
|
||||
expect(res.length).toBe(3);
|
||||
});
|
||||
|
||||
it('Should return both permissions concerning project projet-test', () => {
|
||||
const res = pipe.transform(testPermissions, 'test');
|
||||
expect(res.length).toBe(2);
|
||||
expect(res).toContain(testPermissions[0]);
|
||||
expect(res).toContain(testPermissions[1]);
|
||||
});
|
||||
|
||||
it('Should return no permissions', () => {
|
||||
const res = pipe.transform(testPermissions, 'aaaaaa');
|
||||
expect(res.length).toBe(0);
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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 { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Permission} from "@models/api/permission";
|
||||
|
||||
@Pipe({
|
||||
name: 'permissionsFilter'
|
||||
})
|
||||
export class PermissionsFilterPipe implements PipeTransform {
|
||||
|
||||
transform(permissions: Permission[], filterText: string): Permission[] {
|
||||
if (!permissions) {
|
||||
return [];
|
||||
}
|
||||
if (filterText === undefined || filterText === null || filterText === '') {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
return permissions.filter((permissions: Permission) => permissions.path.toLowerCase().includes(filterText.toLowerCase()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
<div class="content" *ngIf="isReady; else loading">
|
||||
<div class="add">
|
||||
<app-add-permission-line
|
||||
[controller]="controller"
|
||||
(addPermissionEvent)="refresh()"></app-add-permission-line>
|
||||
</div>
|
||||
<div class="permission-content default-content">
|
||||
<input type="text"
|
||||
matInput
|
||||
name="typeInput"
|
||||
placeholder="Search by name"
|
||||
[(ngModel)]="searchPermissions"
|
||||
[matAutocomplete]="auto"
|
||||
class="permission-filter"
|
||||
(input)="changeAutocomplete($event.target.value)">
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
|
||||
<mat-option *ngFor="let option of filteredOptions | filterComplete: searchPermissions" [value]="option">
|
||||
{{option.name}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
|
||||
<div *ngFor="let permission of permissions | permissionsFilter: searchPermissions?.id | paginator: pageEvent">
|
||||
<app-permission-add-edit-line
|
||||
[permission]="permission"
|
||||
[controller]="controller"
|
||||
(update)="refresh()"></app-permission-add-edit-line>
|
||||
</div>
|
||||
<mat-paginator [length]="permissions.length" (page)="pageEvent = $event"
|
||||
[pageSizeOptions]="[5, 20, 50, 100]"></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<div>
|
||||
<mat-spinner class="loader"></mat-spinner>
|
||||
</div>
|
||||
</ng-template>
|
@ -1,36 +0,0 @@
|
||||
.permission-content {
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
height: 40px;
|
||||
width: 160px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
height: 175px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.add {
|
||||
/* display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid;
|
||||
align-items: center;*/
|
||||
}
|
||||
|
||||
.permission-filter {
|
||||
border-bottom: 1px solid;
|
||||
margin: 5px;
|
||||
border-bottom-color: #b0bec5;
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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, ComponentFactoryResolver, OnInit, ViewChild, ViewContainerRef} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {Controller} from "@models/controller";
|
||||
import {PermissionsService} from "@services/permissions.service";
|
||||
import {ProgressService} from "../../common/progress/progress.service";
|
||||
import {Permission} from "@models/api/permission";
|
||||
import {AddPermissionLineComponent} from "@components/permissions-management/add-permission-line/add-permission-line.component";
|
||||
import {ControllerService} from "@services/controller.service";
|
||||
import {PageEvent} from "@angular/material/paginator";
|
||||
import {ApiInformationService} from "@services/ApiInformation/api-information.service";
|
||||
import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject";
|
||||
|
||||
@Component({
|
||||
selector: 'app-permissions-management',
|
||||
templateUrl: './permissions-management.component.html',
|
||||
styleUrls: ['./permissions-management.component.scss']
|
||||
})
|
||||
export class PermissionsManagementComponent implements OnInit {
|
||||
controller: Controller;
|
||||
permissions: Permission[];
|
||||
addPermissionLineComp = AddPermissionLineComponent;
|
||||
newPermissionEdit = false;
|
||||
searchPermissions: any;
|
||||
pageEvent: PageEvent | undefined;
|
||||
filteredOptions: IGenericApiObject[];
|
||||
options: string[] = [];
|
||||
|
||||
@ViewChild('dynamic', {
|
||||
read: ViewContainerRef
|
||||
}) viewContainerRef: ViewContainerRef;
|
||||
isReady = false;
|
||||
|
||||
constructor(private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private permissionService: PermissionsService,
|
||||
private progressService: ProgressService,
|
||||
private controllerService: ControllerService,
|
||||
private apiInformationService: ApiInformationService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const controllerId = this.route.parent.snapshot.paramMap.get('controller_id');
|
||||
this.controllerService.get(+controllerId).then((controller: Controller) => {
|
||||
this.controller = controller;
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.permissionService.list(this.controller).subscribe(
|
||||
(permissions: Permission[]) => {
|
||||
this.permissions = permissions;
|
||||
this.isReady = true;
|
||||
},
|
||||
(error) => {
|
||||
this.progressService.setError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayFn(value): string {
|
||||
return value && value.name ? value.name : '';
|
||||
}
|
||||
|
||||
changeAutocomplete(inputText) {
|
||||
this.filteredOptions = this.apiInformationService.getIdByObjNameFromCache(inputText);
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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 { Pipe, PipeTransform } from '@angular/core';
|
||||
import {Permission} from "@models/api/permission";
|
||||
|
||||
@Pipe({
|
||||
name: 'permissionsTypeFilter'
|
||||
})
|
||||
export class PermissionsTypeFilterPipe implements PipeTransform {
|
||||
|
||||
transform(permissions: Permission[], filterTypeText: string): Permission[] {
|
||||
if (!permissions) {
|
||||
return [];
|
||||
}
|
||||
if (filterTypeText === undefined || filterTypeText === null || filterTypeText === '') {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
return permissions.filter((permissions: Permission) => permissions.path.toLowerCase().includes(filterTypeText.toLowerCase()));
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<table class="table" mat-table [dataSource]="adapters">
|
||||
<table class="table" mat-table [dataSource]="adapters">
|
||||
<ng-container matColumnDef="adapter_number">
|
||||
<th mat-header-cell *matHeaderCellDef>Adapter number</th>
|
||||
<td mat-cell *matCellDef="let element">Adapter {{ element.adapter_number }}</td>
|
||||
@ -6,29 +6,25 @@
|
||||
|
||||
<ng-container matColumnDef="port_name">
|
||||
<th mat-header-cell *matHeaderCellDef>Port name</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.short_name || element.port_name }}</td>
|
||||
<td mat-cell *matCellDef="let element">Ethernet {{ element.adapter_number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="adapter_type">
|
||||
<th mat-header-cell *matHeaderCellDef>Adapter type</th>
|
||||
<td mat-cell *matCellDef="let element; let i = index" style="margin-right: 10px;">
|
||||
<td mat-cell *matCellDef="let element; let i = index">
|
||||
<mat-select placeholder="Type" [(ngModel)]="element.adapter_type">
|
||||
<mat-option *ngFor="let type of networkTypes" [value]="type.value">
|
||||
{{type.name}} ({{type.value}})
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="mac_address">
|
||||
<th mat-header-cell *matHeaderCellDef>MAC address</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.mac_address }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-icon-button matTooltip="Delete adapter" matTooltipClass="custom-tooltip" (click)="delete(element)">
|
||||
<mat-icon aria-label="Delete adapter">delete</mat-icon>
|
||||
<mat-icon aria-label="Delete adapter">delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -17,8 +17,6 @@ export class CustomAdaptersTableComponent {
|
||||
let adapter: CustomAdapter = {
|
||||
adapter_number: this.adapters.length,
|
||||
adapter_type: this.networkTypes[0],
|
||||
mac_address:null,
|
||||
port_name:null
|
||||
};
|
||||
this.adapters = this.adapters.concat([adapter]);
|
||||
}
|
||||
|
@ -30,8 +30,6 @@ export class CustomAdaptersComponent {
|
||||
this.adapters.push({
|
||||
adapter_number: n.adapter_number,
|
||||
adapter_type: n.adapter_type,
|
||||
mac_address:n.mac_address,
|
||||
port_name:n.port_name
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -17,7 +17,6 @@ import { QemuService } from '../../../../services/qemu.service';
|
||||
import { ControllerService } from '../../../../services/controller.service';
|
||||
import { TemplateMocksService } from '../../../../services/template-mocks.service';
|
||||
import { ToasterService } from '../../../../services/toaster.service';
|
||||
import { CustomAdapter } from '@models/qemu/qemu-custom-adapter';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-qemu-virtual-machine-template',
|
||||
@ -176,13 +175,7 @@ export class AddQemuVmTemplateComponent implements OnInit {
|
||||
this.qemuTemplate.template_id = uuid();
|
||||
this.qemuTemplate.name = this.nameForm.get('templateName').value;
|
||||
this.qemuTemplate.compute_id = 'local';
|
||||
// let adapter: CustomAdapter = {
|
||||
// adapter_number: this.qemuTemplate.adapters,
|
||||
// adapter_type:this.qemuTemplate.adapter_type ,
|
||||
// mac_address:null,
|
||||
// port_name:this.qemuTemplate.port_name_format
|
||||
// };
|
||||
// this.qemuTemplate.custom_adapters = this.qemuTemplate.custom_adapters.concat([adapter]);
|
||||
|
||||
this.qemuService.addTemplate(this.controller, this.qemuTemplate).subscribe((template: QemuTemplate) => {
|
||||
this.goBack();
|
||||
});
|
||||
|
@ -103,12 +103,10 @@ export class QemuVmTemplateDetailsComponent implements OnInit {
|
||||
this.fillCustomAdapters();
|
||||
this.customAdaptersConfigurator.numberOfAdapters = this.qemuTemplate.adapters;
|
||||
this.customAdaptersConfigurator.adapters = [];
|
||||
this.qemuTemplate.custom_adapters.forEach((adapter: CustomAdapter,i) => {
|
||||
this.qemuTemplate.custom_adapters.forEach((adapter: CustomAdapter) => {
|
||||
this.customAdaptersConfigurator.adapters.push({
|
||||
adapter_number: adapter.adapter_number,
|
||||
adapter_type: adapter.adapter_type,
|
||||
mac_address:null,
|
||||
port_name:this.qemuTemplate.port_name_format +'/'+ `${i}`
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -120,18 +118,16 @@ export class QemuVmTemplateDetailsComponent implements OnInit {
|
||||
}
|
||||
|
||||
fillCustomAdapters() {
|
||||
|
||||
let copyOfAdapters = this.qemuTemplate.custom_adapters ? this.qemuTemplate.custom_adapters : [];
|
||||
this.qemuTemplate.custom_adapters = [];
|
||||
|
||||
for (let i = 0; i < this.qemuTemplate.adapters; i++) {
|
||||
if (copyOfAdapters[i]) {
|
||||
this.qemuTemplate.custom_adapters.push(copyOfAdapters[i]);
|
||||
} else {
|
||||
this.qemuTemplate.custom_adapters.push({
|
||||
adapter_number: i,
|
||||
adapter_type: this.qemuTemplate.adapter_type,
|
||||
mac_address:null,
|
||||
port_name:this.qemuTemplate.port_name_format.replace(/[0-9]/g,`${i}`)
|
||||
adapter_type: 'e1000',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -88,9 +88,6 @@ export class VirtualBoxTemplateDetailsComponent implements OnInit {
|
||||
this.customAdaptersConfigurator.adapters.push({
|
||||
adapter_number: adapter.adapter_number,
|
||||
adapter_type: adapter.adapter_type,
|
||||
mac_address:adapter.mac_address,
|
||||
port_name:adapter.port_name
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -112,8 +109,6 @@ export class VirtualBoxTemplateDetailsComponent implements OnInit {
|
||||
this.virtualBoxTemplate.custom_adapters.push({
|
||||
adapter_number: i,
|
||||
adapter_type: 'e1000',
|
||||
mac_address:'',
|
||||
port_name:''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -94,8 +94,6 @@ export class VmwareTemplateDetailsComponent implements OnInit {
|
||||
this.customAdaptersConfigurator.adapters.push({
|
||||
adapter_number: adapter.adapter_number,
|
||||
adapter_type: adapter.adapter_type,
|
||||
mac_address:adapter.mac_address,
|
||||
port_name:adapter.port_name
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -117,8 +115,6 @@ export class VmwareTemplateDetailsComponent implements OnInit {
|
||||
this.vmwareTemplate.custom_adapters.push({
|
||||
adapter_number: i,
|
||||
adapter_type: 'e1000',
|
||||
mac_address:'',
|
||||
port_name:''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<h1 mat-dialog-title>Configure custom adapters for node {{ node.name }}</h1>
|
||||
|
||||
<div *ngIf="node" class="modal-form-container">
|
||||
@ -9,11 +8,11 @@
|
||||
|
||||
<div>
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let adapter of node.ports; index as i">
|
||||
<mat-list-item *ngFor="let adapter of adapters; index as i">
|
||||
<div class="header">
|
||||
<span class="column"> Adapter {{ adapter.adapter_number }} </span>
|
||||
<span class="column">
|
||||
<input matInput type="text" [(ngModel)]="adapter.short_name " placeholder="Edit port name" readonly/>
|
||||
<input matInput type="text" [(ngModel)]="adapter.port_name" placeholder="Edit port name" />
|
||||
</span>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
|
@ -27,7 +27,7 @@ export class ConfiguratorDialogQemuComponent implements OnInit {
|
||||
bootPriorities = [];
|
||||
diskInterfaces: string[] = [];
|
||||
|
||||
displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type','mac_address','actions'];
|
||||
displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions'];
|
||||
networkTypes = [];
|
||||
qemuImages: QemuImage[] = [];
|
||||
selectPlatform: string[] = [];
|
||||
@ -105,12 +105,10 @@ export class ConfiguratorDialogQemuComponent implements OnInit {
|
||||
onSaveClick() {
|
||||
if (this.generalSettingsForm.valid) {
|
||||
this.node.custom_adapters = [];
|
||||
this.customAdapters.adapters.forEach((n,i) => {
|
||||
this.customAdapters.adapters.forEach((n) => {
|
||||
this.node.custom_adapters.push({
|
||||
adapter_number: n.adapter_number,
|
||||
adapter_type: n.adapter_type,
|
||||
mac_address: n.mac_address ?? n.mac_address,
|
||||
port_name: n.port_name ?? this.node.port_name_format.replace(/[0-9]/g,`${i}`)
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<form>
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Search by name" [(ngModel)]="searchText" [ngModelOptions]="{ standalone: true }" />
|
||||
<input matInput placeholder="Search project by name" [(ngModel)]="searchText" [ngModelOptions]="{ standalone: true }" />
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
<div class="title">delete resource {{data.resource_type}}/{{data.name}} ?</div>
|
||||
<div class="button">
|
||||
<button mat-raised-button color="warn" (click)="dialogRef.close()">Cancel</button>
|
||||
<button mat-raised-button color="primary" (click)="dialogRef.close(data)">Update</button>
|
||||
</div>
|
@ -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;
|
||||
}
|
@ -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<DeleteResourceConfirmationDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DeleteResourceConfirmationDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DeleteResourceConfirmationDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<DeleteResourceConfirmationDialogComponent>,) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<div class="content">
|
||||
<div class="default-header">
|
||||
<div class="row align-items-center">
|
||||
<a
|
||||
mat-icon-button
|
||||
matTooltip="back to resource pools management"
|
||||
mattooltipclass="custom-tooltip"
|
||||
[routerLink]="['/controller', controller.id, 'management', 'resourcePools']">
|
||||
<mat-icon aria-label="back to resource pools management">keyboard_arrow_left</mat-icon>
|
||||
</a>
|
||||
<h1 class="col">Resource pool {{pool.name}}</h1>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="details">
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Resource pool name:</mat-label>
|
||||
<input matInput type="text" [(ngModel)]="pool.name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>creation date: {{pool.created_at}}</div>
|
||||
<div>last update date: {{pool.updated_at}}</div>
|
||||
<div>uuid: {{pool.resource_pool_id}}</div>
|
||||
<div mat-dialog-actions class="button-div">
|
||||
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary">
|
||||
Update resource pool
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<div class="resources">
|
||||
<div class="addResource">
|
||||
<div >
|
||||
<input type="text"
|
||||
placeholder="add project to resource pool"
|
||||
matInput
|
||||
[formControl]="addResourceFormControl"
|
||||
[matAutocomplete]="auto">
|
||||
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
|
||||
<mat-option *ngFor="let option of addResourceFilteredOptions | async" [value]="option">
|
||||
{{option}}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-button>
|
||||
<mat-icon (click)="addResource()">add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ownedResources" *ngFor="let resource of pool.resources">
|
||||
<div>{{resource.name}}</div>
|
||||
<div>
|
||||
<button mat-button>
|
||||
<mat-icon (click)="deleteResource(resource)">delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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;
|
||||
}
|
@ -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<ResourcePoolDetailsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ResourcePoolDetailsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ResourcePoolDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<string[]>;
|
||||
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);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
@ -10,15 +10,17 @@
|
||||
*
|
||||
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||
*/
|
||||
export class SubPath {
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* @param {value} original subPath value from gns3 api
|
||||
* @param {displayValue} displayed value can replace a UUID from original URL
|
||||
* @param {key} associate key ex: 'project_id'
|
||||
*/
|
||||
constructor(public value: string,
|
||||
public displayValue: string,
|
||||
public key?: string) {
|
||||
}
|
||||
@Injectable()
|
||||
export class PoolNameValidator {
|
||||
get(poolName) {
|
||||
const pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/);
|
||||
|
||||
if (!pattern.test(poolName.value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { invalidName: true };
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<h1 mat-dialog-title>Create new resource pool</h1>
|
||||
<form [formGroup]="poolNameForm" class="file-name-form">
|
||||
<mat-form-field class="file-name-form-field">
|
||||
<input
|
||||
matInput
|
||||
(keydown)="onKeyDown($event)"
|
||||
type="text"
|
||||
formControlName="poolName"
|
||||
[ngClass]="{ 'is-invalid': form.poolName?.errors }"
|
||||
placeholder="Please enter a resource pool name"
|
||||
/>
|
||||
<mat-error *ngIf="form.poolName?.touched && form.poolName?.errors && form.poolName?.errors.required"
|
||||
>Resource pool name is required</mat-error
|
||||
>
|
||||
<mat-error *ngIf="form.poolName?.errors && form.poolName?.errors.invalidName"
|
||||
>Resource pool name is incorrect</mat-error
|
||||
>
|
||||
<mat-error *ngIf="form.poolName?.errors && form.poolName?.errors.projectExist"
|
||||
>Resource pool with this name exists</mat-error
|
||||
>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<div mat-dialog-actions class="button-div">
|
||||
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
|
||||
<button mat-button (click)="onAddClick()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||
Add resource pool
|
||||
</button>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
@ -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<AddResourcePoolDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AddResourcePoolDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddResourcePoolDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<AddResourcePoolDialogComponent>,
|
||||
@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();
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<h1 mat-dialog-title>Are you sure to delete pools named: </h1>
|
||||
<p *ngFor="let pool of data.pools">{{pool.name}}</p>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel()" color="accent">No, cancel</button>
|
||||
<button mat-button (click)="onDelete()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||
Yes, delete!
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
@ -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<DeleteResourcePoolComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DeleteResourcePoolComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DeleteResourcePoolComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -10,28 +10,31 @@
|
||||
*
|
||||
* Author: Sylvain MATHIEU, Elise LEBEAU
|
||||
*/
|
||||
|
||||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {Permission} from "@models/api/permission";
|
||||
import {ResourcePool} from "@models/resourcePools/ResourcePool";
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-permission-dialog',
|
||||
templateUrl: './delete-permission-dialog.component.html',
|
||||
styleUrls: ['./delete-permission-dialog.component.scss']
|
||||
selector: 'app-delete-resource-pool',
|
||||
templateUrl: './delete-resource-pool.component.html',
|
||||
styleUrls: ['./delete-resource-pool.component.scss']
|
||||
})
|
||||
export class DeletePermissionDialogComponent implements OnInit {
|
||||
export class DeleteResourcePoolComponent implements OnInit {
|
||||
|
||||
constructor(private dialog: MatDialogRef<DeletePermissionDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Permission) { }
|
||||
constructor(private dialogRef: MatDialogRef<DeleteResourcePoolComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { pools: ResourcePool[] }) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dialog.close(false);
|
||||
onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.dialog.close(true);
|
||||
onDelete() {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { ResourcePoolsFilterPipe } from './resource-pools-filter.pipe';
|
||||
|
||||
describe('ResourcePoolsFilterPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new ResourcePoolsFilterPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<ResourcePool>, searchText: string): MatTableDataSource<ResourcePool> {
|
||||
if (!searchText) {
|
||||
return resourcePool;
|
||||
}
|
||||
|
||||
searchText = searchText.trim().toLowerCase();
|
||||
resourcePool.filter = searchText;
|
||||
return resourcePool;
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<div class="content" *ngIf="isReady; else loading">
|
||||
<div class="default-header">
|
||||
<div class="row">
|
||||
<h1 class="col">Resource pools</h1>
|
||||
<button class="col" mat-raised-button color="primary" (click)="onDelete(selection.selected)"
|
||||
class="add-ressourcePool-button" [disabled]="selection.selected.length == 0">
|
||||
Delete selected pools
|
||||
</button>
|
||||
<button class="col" mat-raised-button color="primary" (click)="addResourcePool()" class="add-ressourcePool-button">
|
||||
Add resource pool
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Search resource pool by name" [(ngModel)]="searchText"
|
||||
[ngModelOptions]="{ standalone: true }"/>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<div class="default-content">
|
||||
<table mat-table [dataSource]="dataSource | resourcePoolsFilter: searchText" class="mat-elevation-z8" matSort
|
||||
#resourcePoolsSort="matSort">
|
||||
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef class="small-col">
|
||||
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row" class="small-col">
|
||||
<mat-checkbox (click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(row) : null"
|
||||
[checked]="selection.isSelected(row)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name</th>
|
||||
<td mat-cell *matCellDef="let element"><a class="table-link"
|
||||
routerLink="/controller/{{controller.id}}/management/resourcePools/{{element.resource_pool_id}}">{{element.name}}</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="created_at">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Creation date</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.created_at}} </td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container matColumnDef="updated_at">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last update</th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.updated_at}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="delete">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<button mat-button (click)="onDelete([element])">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<mat-paginator #resourcePoolsPaginator="matPaginator"
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
aria-label="Select page">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<div>
|
||||
<mat-spinner class="loader"></mat-spinner>
|
||||
</div>
|
||||
</ng-template>
|
@ -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;
|
||||
}
|
@ -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<ResourcePoolsManagementComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ ResourcePoolsManagementComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ResourcePoolsManagementComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -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<MatPaginator>;
|
||||
@ViewChildren('resourcePoolsSort') resourcePoolsSort: QueryList<MatSort>;
|
||||
|
||||
public displayedColumns = ['select', 'name', 'created_at', 'updated_at', 'delete'];
|
||||
selection = new SelectionModel<ResourcePool>(true, []);
|
||||
resourcePools: ResourcePool[];
|
||||
dataSource = new MatTableDataSource<ResourcePool>();
|
||||
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 <MatPaginator>) =>
|
||||
{
|
||||
this.dataSource.paginator = comps.first;
|
||||
});
|
||||
this.resourcePoolsSort.changes.subscribe((comps: QueryList<MatSort>) => {
|
||||
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`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<div [matTooltip]="getToolTip()"
|
||||
matTooltipClass="permission-tooltip"
|
||||
class="box" [ngClass]="{allow: permission.action === 'ALLOW', left: side === 'LEFT'}">
|
||||
<button *ngIf="side === 'RIGHT'" mat-button (click)="onClick()">
|
||||
<mat-icon>keyboard_arrow_left</mat-icon>
|
||||
</button>
|
||||
<div class="content">
|
||||
<div class="center">{{permission.methods.join(",")}}</div>
|
||||
<div class="center">{{permission.path | displayPath: controller | async}}</div>
|
||||
</div>
|
||||
<button *ngIf="side === 'LEFT'" mat-button (click)="onClick()">
|
||||
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||
</button>
|
||||
|
||||
</div>
|
@ -1,42 +0,0 @@
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid;
|
||||
border-radius: 20px;
|
||||
margin: 10px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
justify-items: center;
|
||||
background-color: rgba(130, 8, 8, 0.36);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.left {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.allow {
|
||||
background-color: rgba(5, 76, 5, 0.38);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.content > div {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
button {
|
||||
border-radius: 20px;
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {Permission} from "@models/api/permission";
|
||||
import {Controller} from '@models/controller';
|
||||
|
||||
@Component({
|
||||
selector: 'app-editable-permission',
|
||||
templateUrl: './editable-permission.component.html',
|
||||
styleUrls: ['./editable-permission.component.scss']
|
||||
})
|
||||
export class EditablePermissionComponent implements OnInit {
|
||||
|
||||
@Input() permission: Permission;
|
||||
@Input() controller: Controller;
|
||||
@Input() side: 'LEFT' | 'RIGHT';
|
||||
@Output() click = new EventEmitter();
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
|
||||
onClick() {
|
||||
this.click.emit();
|
||||
}
|
||||
|
||||
getToolTip() {
|
||||
return `
|
||||
action: ${this.permission.action}
|
||||
methods: ${this.permission.methods.join(',')}
|
||||
original path: ${this.permission.path}
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<div>
|
||||
<div class="button">
|
||||
<button
|
||||
mat-button
|
||||
mat-raised-button
|
||||
(click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button *ngIf="data.add.length > 0 || data.remove.length > 0"
|
||||
mat-button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="update()">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<div class="change" *ngIf="data.add.length > 0 || data.remove.length > 0; else nothingTodo">
|
||||
<p></p>
|
||||
<div class="title" *ngIf="data.add.length > 0">Permission to Add:</div>
|
||||
<div *ngFor="let permission of data.add">
|
||||
<app-editable-permission [permission]="permission"></app-editable-permission>
|
||||
</div>
|
||||
<div class="title" *ngIf="data.remove.length > 0">Permission to Remove:</div>
|
||||
<div *ngFor="let permission of data.remove">
|
||||
<app-editable-permission [permission]="permission"></app-editable-permission>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #nothingTodo>
|
||||
<div class="noChange">
|
||||
No change
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user