diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 362fcbf1..07b0f260 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -57,6 +57,23 @@ import { ControllerResolve } from './resolvers/controller-resolve';
import { UserManagementComponent } from './components/user-management/user-management.component';
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
import { ImageManagerComponent } from './components/image-manager/image-manager.component';
+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";
const routes: Routes = [
{
@@ -78,6 +95,16 @@ const routes: Routes = [
{ path: 'help', component: HelpComponent },
{ path: 'settings', component: SettingsComponent },
{ path: 'settings/console', component: ConsoleComponent },
+ {
+ path: 'controller/:controller_id/management/users/:user_id',
+ component: UserDetailComponent,
+ canActivate: [LoginGuard],
+ resolve: {
+ user: UserDetailResolver,
+ groups: UserGroupsResolver,
+ permissions: UserPermissionsResolver,
+ server: ServerResolve},
+ },
{ path: 'installed-software', component: InstalledSoftwareComponent },
{ path: 'controller/:controller_id/systemstatus', component: SystemStatusComponent, canActivate: [LoginGuard] },
@@ -187,11 +214,72 @@ const routes: Routes = [
canActivate: [LoginGuard]
},
{ path: 'controller/:controller_id/preferences/docker/addtemplate', component: AddDockerTemplateComponent, canActivate: [LoginGuard] },
-
{ path: 'controller/:controller_id/preferences/iou/templates', component: IouTemplatesComponent, canActivate: [LoginGuard] },
- { path: 'controller/:controller_id/preferences/iou/templates/:template_id', component: IouTemplateDetailsComponent, canActivate: [LoginGuard] },
- { path: 'controller/:controller_id/preferences/iou/templates/:template_id/copy', component: CopyIouTemplateComponent, canActivate: [LoginGuard] },
+ { path: 'controller/:controller_id//preferences/iou/templates/:template_id', component: IouTemplateDetailsComponent, canActivate: [LoginGuard] },
+ {
+ path: 'controller/:controller_id/preferences/iou/templates/:template_id/copy',
+ component: CopyIouTemplateComponent,
+ canActivate: [LoginGuard]
+ },
{ path: 'controller/:controller_id/preferences/iou/addtemplate', component: AddIouTemplateComponent, canActivate: [LoginGuard] },
+ {
+ path: 'controller/:controller_id/management',
+ component: ManagementComponent,
+ children: [
+ {
+ path: 'users',
+ component: UserManagementComponent
+ },
+ {
+ path: 'groups',
+ component: GroupManagementComponent
+ },
+ {
+ path: 'roles',
+ component: RoleManagementComponent
+ },
+ {path: 'permissions',
+ component: PermissionsManagementComponent
+ }
+ ]
+ },
+ {
+ path: 'controller/:controller_id/management/groups/:user_group_id',
+ component: GroupDetailsComponent,
+ resolve: {
+ members: GroupMembersResolver,
+ server: ServerResolve,
+ group: GroupResolver,
+ roles: GroupRoleResolver
+ }
+ },
+ {
+ path: 'controller/:controller_id/management/roles/:role_id',
+ component: RoleDetailComponent,
+ resolve: {
+ role: RoleDetailResolver,
+ server: ServerResolve
+ }
+ },
+ {
+ path: 'controller/:controller_id/management/roles/:role_id/permissions',
+ component: RolePermissionsComponent,
+ resolve: {
+ role: RoleDetailResolver,
+ server: ServerResolve,
+ permissions: PermissionResolver
+ }
+ },
+ {
+ path: 'controller/:controller_id/management/users/:user_id/permissions',
+ component: UserPermissionsComponent,
+ resolve: {
+ user: UserDetailResolver,
+ userPermissions: UserPermissionsResolver,
+ server: ServerResolve,
+ permissions: PermissionResolver
+ }
+ }
],
},
{
@@ -210,10 +298,6 @@ const routes: Routes = [
component: WebConsoleFullWindowComponent,
canActivate: [LoginGuard]
},
- {
- path: 'user_management',
- component: UserManagementComponent
- },
{
path: '**',
component: PageNotFoundComponent,
@@ -231,4 +315,5 @@ const routes: Routes = [
],
exports: [RouterModule],
})
-export class AppRoutingModule {}
+export class AppRoutingModule {
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 1b45568c..77bc4acd 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,3 +1,4 @@
+/* tslint:disable */
import { DragDropModule } from '@angular/cdk/drag-drop';
import { OverlayModule } from '@angular/cdk/overlay';
import { CdkTableModule } from '@angular/cdk/table';
@@ -132,7 +133,7 @@ import { StartNodeActionComponent } from './components/project-map/context-menu/
import { StopCaptureActionComponent } from './components/project-map/context-menu/actions/stop-capture/stop-capture-action.component';
import { IsolateNodeActionComponent } from './components/project-map/context-menu/actions/isolate-node-action/isolate-node-action.component';
import { UnisolateNodeActionComponent } from './components/project-map/context-menu/actions/unisolate-node-action/unisolate-node-action.component';
-import { StopNodeActionComponent } from './components/project-map/context-menu/actions/stop-node-action/stop-node-action.component';
+import {StopNodeActionComponent } from './components/project-map/context-menu/actions/stop-node-action/stop-node-action.component';
import { SuspendLinkActionComponent } from './components/project-map/context-menu/actions/suspend-link/suspend-link-action.component';
import { SuspendNodeActionComponent } from './components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component';
import { ContextMenuComponent } from './components/project-map/context-menu/context-menu.component';
@@ -271,9 +272,50 @@ import { MarkedDirective } from './directives/marked.directive';
import { LoginComponent } from './components/login/login.component';
import { LoginService } from './services/login.service';
import { HttpRequestsInterceptor } from './interceptors/http.interceptor';
-import { UserManagementComponent } from './components/user-management/user-management.component'
+import { UserManagementComponent } from './components/user-management/user-management.component';
import { UserService } from './services/user.service';
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
+import { AddUserDialogComponent } from './components/user-management/add-user-dialog/add-user-dialog.component';
+import { UserFilterPipe } from './filters/user-filter.pipe';
+import { GroupManagementComponent } from './components/group-management/group-management.component';
+import { GroupFilterPipe } from './filters/group-filter.pipe';
+import { AddGroupDialogComponent } from './components/group-management/add-group-dialog/add-group-dialog.component';
+import { DeleteGroupDialogComponent } from './components/group-management/delete-group-dialog/delete-group-dialog.component';
+import { DeleteUserDialogComponent } from './components/user-management/delete-user-dialog/delete-user-dialog.component';
+import { GroupDetailsComponent } from './components/group-details/group-details.component';
+import { UserDetailComponent } from './components/user-management/user-detail/user-detail.component';
+import { AddUserToGroupDialogComponent } from './components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component';
+import { RemoveToGroupDialogComponent } from '@components/group-details/remove-to-group-dialog/remove-to-group-dialog.component';
+import { PaginatorPipe } from './components/group-details/paginator.pipe';
+import { MembersFilterPipe } from './components/group-details/members-filter.pipe';
+import { ManagementComponent } from './components/management/management.component';
+import {MatCheckboxModule} from "@angular/material/checkbox";
+import { RoleManagementComponent } from './components/role-management/role-management.component';
+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 { 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';
import { AddImageDialogComponent } from './components/image-manager/add-image-dialog/add-image-dialog.component';
import { DeleteAllImageFilesDialogComponent } from './components/image-manager/deleteallfiles-dialog/deleteallfiles-dialog.component';
@@ -470,6 +512,47 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
EditNetworkConfigurationDialogComponent,
UserManagementComponent,
ProjectReadmeComponent,
+ AddGroupDialogComponent,
+ GroupFilterPipe,
+ GroupManagementComponent,
+ AddUserDialogComponent,
+ UserFilterPipe,
+ DeleteGroupDialogComponent,
+ DeleteUserDialogComponent,
+ GroupDetailsComponent,
+ UserDetailComponent,
+ AddUserToGroupDialogComponent,
+ RemoveToGroupDialogComponent,
+ PaginatorPipe,
+ MembersFilterPipe,
+ ManagementComponent,
+ RoleManagementComponent,
+ RoleFilterPipe,
+ 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,
AddImageDialogComponent,
DeleteAllImageFilesDialogComponent,
@@ -489,6 +572,8 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
NgxElectronModule,
FileUploadModule,
MatSidenavModule,
+ MatFormFieldModule,
+ MatMenuModule,
ResizableModule,
DragAndDropModule,
DragDropModule,
@@ -496,11 +581,14 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
MATERIAL_IMPORTS,
NgCircleProgressModule.forRoot(),
OverlayModule,
+ MatSlideToggleModule,
+ MatCheckboxModule,
+ MatAutocompleteModule,
],
providers: [
SettingsService,
- { provide: ErrorHandler, useClass: ToasterErrorHandler },
- { provide: HTTP_INTERCEPTORS, useClass: HttpRequestsInterceptor, multi: true },
+ {provide: ErrorHandler, useClass: ToasterErrorHandler},
+ {provide: HTTP_INTERCEPTORS, useClass: HttpRequestsInterceptor, multi: true},
D3Service,
VersionService,
ProjectService,
diff --git a/src/app/components/group-details/add-role-to-group/add-role-to-group.component.html b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.html
new file mode 100644
index 00000000..dff5bcd0
--- /dev/null
+++ b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.html
@@ -0,0 +1,15 @@
+
+
Add Role To group: {{data.group.name}}
+
+
+
+ Search user
+
+
+
+
+
+
{{role.name}}
+
add
+
+
diff --git a/src/app/components/group-details/add-role-to-group/add-role-to-group.component.scss b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.scss
new file mode 100644
index 00000000..01cd6a62
--- /dev/null
+++ b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.scss
@@ -0,0 +1,35 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.title {
+ width: 100%;
+ text-align: center;
+}
+
+.filter {
+ display: flex;
+ width: 600px;
+ justify-content: center;
+ margin-bottom: 50px;
+}
+
+mat-form-field {
+ width: 600px;
+}
+
+input {
+ width: 100%;
+}
+
+.userList {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+mat-spinner {
+ width: 36px;
+}
diff --git a/src/app/components/group-details/add-role-to-group/add-role-to-group.component.spec.ts b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-details/add-role-to-group/add-role-to-group.component.ts b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.ts
new file mode 100644
index 00000000..e6a34e04
--- /dev/null
+++ b/src/app/components/group-details/add-role-to-group/add-role-to-group.component.ts
@@ -0,0 +1,90 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {BehaviorSubject, forkJoin, timer} from "rxjs";
+import {User} from "@models/users/user";
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {Server} from "@models/server";
+import {Group} from "@models/groups/group";
+import {UserService} from "@services/user.service";
+import {GroupService} from "@services/group.service";
+import {ToasterService} from "@services/toaster.service";
+import {Role} from "@models/api/role";
+import {RoleService} from "@services/role.service";
+
+@Component({
+ selector: 'app-add-role-to-group',
+ templateUrl: './add-role-to-group.component.html',
+ styleUrls: ['./add-role-to-group.component.scss']
+})
+export class AddRoleToGroupComponent implements OnInit {
+ roles = new BehaviorSubject([]);
+ displayedRoles = new BehaviorSubject([]);
+
+ searchText: string;
+ loading = false;
+
+ constructor(private dialog: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { server: Server; group: Group },
+ private groupService: GroupService,
+ private roleService: RoleService,
+ private toastService: ToasterService) {
+ }
+
+ ngOnInit(): void {
+ this.getRoles();
+ }
+
+ onSearch() {
+ timer(500)
+ .subscribe(() => {
+ const displayedUsers = this.roles.value.filter((roles: Role) => {
+ return roles.name.includes(this.searchText);
+ });
+
+ this.displayedRoles.next(displayedUsers);
+ });
+ }
+
+ getRoles() {
+ forkJoin([
+ this.roleService.get(this.data.server),
+ this.groupService.getGroupRole(this.data.server, this.data.group.user_group_id)
+ ]).subscribe((results) => {
+ const [globalRoles, groupRoles] = results;
+ const roles = globalRoles.filter((role: Role) => {
+ return !groupRoles.find((r: Role) => r.role_id === role.role_id);
+ });
+
+ this.roles.next(roles);
+ this.displayedRoles.next(roles);
+
+ });
+
+ }
+
+ addRole(role: Role) {
+ this.loading = true;
+ this.groupService
+ .addRoleToGroup(this.data.server, this.data.group, role)
+ .subscribe(() => {
+ this.toastService.success(`role ${role.name} was added`);
+ this.getRoles();
+ this.loading = false;
+ }, (err) => {
+ console.log(err);
+ this.toastService.error(`error while adding role ${role.name} to group ${this.data.group.name}`);
+ this.loading = false;
+ });
+ }
+}
diff --git a/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.html b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.html
new file mode 100644
index 00000000..5c397473
--- /dev/null
+++ b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.html
@@ -0,0 +1,16 @@
+
+
Add User To group: {{data.group.name}}
+
+
+
+ Search user
+
+
+
+
+
+
{{user.username}}
+
{{user.email}}
+
add
+
+
diff --git a/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.scss b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.scss
new file mode 100644
index 00000000..01cd6a62
--- /dev/null
+++ b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.scss
@@ -0,0 +1,35 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.title {
+ width: 100%;
+ text-align: center;
+}
+
+.filter {
+ display: flex;
+ width: 600px;
+ justify-content: center;
+ margin-bottom: 50px;
+}
+
+mat-form-field {
+ width: 600px;
+}
+
+input {
+ width: 100%;
+}
+
+.userList {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+mat-spinner {
+ width: 36px;
+}
diff --git a/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.spec.ts b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.ts b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.ts
new file mode 100644
index 00000000..3f1483cb
--- /dev/null
+++ b/src/app/components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component.ts
@@ -0,0 +1,91 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {UserService} from "@services/user.service";
+import {Server} from "@models/server";
+import {BehaviorSubject, forkJoin, observable, Observable, timer} from "rxjs";
+import {User} from "@models/users/user";
+import {GroupService} from "@services/group.service";
+import {Group} from "@models/groups/group";
+import {tap} from "rxjs/operators";
+import {ToasterService} from "@services/toaster.service";
+
+@Component({
+ selector: 'app-add-user-to-group-dialog',
+ templateUrl: './add-user-to-group-dialog.component.html',
+ styleUrls: ['./add-user-to-group-dialog.component.scss']
+})
+export class AddUserToGroupDialogComponent implements OnInit {
+ users = new BehaviorSubject([]);
+ displayedUsers = new BehaviorSubject([]);
+
+ searchText: string;
+ loading = false;
+
+ constructor(private dialog: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { server: Server; group: Group },
+ private userService: UserService,
+ private groupService: GroupService,
+ private toastService: ToasterService) {
+ }
+
+ ngOnInit(): void {
+ this.getUsers();
+ }
+
+ onSearch() {
+ timer(500)
+ .subscribe(() => {
+ const displayedUsers = this.users.value.filter((user: User) => {
+ return user.username.includes(this.searchText) || user.email?.includes(this.searchText);
+ });
+
+ this.displayedUsers.next(displayedUsers);
+ });
+ }
+
+ getUsers() {
+ forkJoin([
+ this.userService.list(this.data.server),
+ this.groupService.getGroupMember(this.data.server, this.data.group.user_group_id)
+ ]).subscribe((results) => {
+ const [userList, members] = results;
+ const users = userList.filter((user: User) => {
+ return !members.find((u: User) => u.user_id === user.user_id);
+ });
+
+ this.users.next(users);
+ this.displayedUsers.next(users);
+
+ });
+
+ }
+
+ addUser(user: User) {
+ this.loading = true;
+ this.groupService
+ .addMemberToGroup(this.data.server, this.data.group, user)
+ .subscribe(() => {
+ this.toastService.success(`user ${user.username} was added`);
+ this.getUsers();
+ this.loading = false;
+ }, (err) => {
+ console.log(err);
+ this.toastService.error(`error while adding user ${user.username} to group ${this.data.group.name}`);
+ this.loading = false;
+ });
+
+
+ }
+}
diff --git a/src/app/components/group-details/group-details.component.html b/src/app/components/group-details/group-details.component.html
new file mode 100644
index 00000000..c8dfd913
--- /dev/null
+++ b/src/app/components/group-details/group-details.component.html
@@ -0,0 +1,72 @@
+
+
+
diff --git a/src/app/components/group-details/group-details.component.scss b/src/app/components/group-details/group-details.component.scss
new file mode 100644
index 00000000..e8417eab
--- /dev/null
+++ b/src/app/components/group-details/group-details.component.scss
@@ -0,0 +1,51 @@
+.main {
+ display: flex;
+ justify-content: space-around;
+}
+
+.details {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.members {
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+}
+
+.members > div {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-bottom: 5px;
+}
+
+.clickable {
+ cursor: pointer;
+}
+
+.details > div {
+ margin-bottom: 20px;
+}
+
+.button-div {
+ float: right;
+}
+
+.members > .search {
+ display: flex;
+ flex-direction: row;
+ justify-content: stretch;
+ width: 100%;
+}
+mat-form-field {
+ width: 100%;
+}
+
+.roles {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
diff --git a/src/app/components/group-details/group-details.component.spec.ts b/src/app/components/group-details/group-details.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-details/group-details.component.ts b/src/app/components/group-details/group-details.component.ts
new file mode 100644
index 00000000..831a1af5
--- /dev/null
+++ b/src/app/components/group-details/group-details.component.ts
@@ -0,0 +1,152 @@
+/*
+* 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, OnInit} from '@angular/core';
+import {ActivatedRoute} from "@angular/router";
+import {Server} from "@models/server";
+import {Group} from "@models/groups/group";
+import {User} from "@models/users/user";
+import {FormControl, FormGroup} from "@angular/forms";
+import {MatDialog} from "@angular/material/dialog";
+import {AddUserToGroupDialogComponent} from "@components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component";
+import {RemoveToGroupDialogComponent} from "@components/group-details/remove-to-group-dialog/remove-to-group-dialog.component";
+import {GroupService} from "@services/group.service";
+import {ToasterService} from "@services/toaster.service";
+import {PageEvent} from "@angular/material/paginator";
+import {Role} from "@models/api/role";
+import {AddRoleToGroupComponent} from "@components/group-details/add-role-to-group/add-role-to-group.component";
+
+@Component({
+ selector: 'app-group-details',
+ templateUrl: './group-details.component.html',
+ styleUrls: ['./group-details.component.scss']
+})
+export class GroupDetailsComponent implements OnInit {
+ server: Server;
+ group: Group;
+ members: User[];
+ editGroupForm: FormGroup;
+ pageEvent: PageEvent | undefined;
+ searchMembers: string;
+ roles: Role[];
+
+ constructor(private route: ActivatedRoute,
+ private dialog: MatDialog,
+ private groupService: GroupService,
+ private toastService: ToasterService) {
+
+ this.editGroupForm = new FormGroup({
+ groupname: new FormControl(''),
+ });
+
+ this.route.data.subscribe((d: { server: Server; group: Group, members: User[], roles: Role[] }) => {
+
+ this.server = d.server;
+ this.group = d.group;
+ this.roles = d.roles;
+ 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 {
+
+ }
+
+ onUpdate() {
+ this.groupService.update(this.server, this.group)
+ .subscribe(() => {
+ this.toastService.success(`group updated`);
+ }, (error) => {
+ this.toastService.error('Error: Cannot update group');
+ console.log(error);
+ });
+ }
+
+ openAddRoleDialog() {
+ this.dialog
+ .open(AddRoleToGroupComponent,
+ {
+ width: '700px', height: '500px',
+ data: {server: this.server, group: this.group}
+ })
+ .afterClosed()
+ .subscribe(() => {
+ this.reloadRoles();
+ });
+ }
+ openAddUserDialog() {
+ this.dialog
+ .open(AddUserToGroupDialogComponent,
+ {
+ width: '700px', height: '500px',
+ data: {server: this.server, group: this.group}
+ })
+ .afterClosed()
+ .subscribe(() => {
+ this.reloadMembers();
+ });
+ }
+
+ openRemoveUserDialog(user: User) {
+ this.dialog.open(RemoveToGroupDialogComponent,
+ {width: '500px', height: '200px', data: {name: user.username}})
+ .afterClosed()
+ .subscribe((confirm: boolean) => {
+ if (confirm) {
+ this.groupService.removeUser(this.server, this.group, user)
+ .subscribe(() => {
+ this.toastService.success(`User ${user.username} was removed`);
+ this.reloadMembers();
+ },
+ (error) => {
+ this.toastService.error(`Error while removing user ${user.username} from ${this.group.name}`);
+ console.log(error);
+ });
+ }
+ });
+ }
+
+
+ openRemoveRoleDialog(role: Role) {
+ this.dialog.open(RemoveToGroupDialogComponent,
+ {width: '500px', height: '200px', data: {name: role.name}})
+ .afterClosed()
+ .subscribe((confirm: string) => {
+ if (confirm) {
+ this.groupService.removeRole(this.server, 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.server, this.group.user_group_id)
+ .subscribe((members: User[]) => {
+ this.members = members;
+ });
+ }
+
+ reloadRoles() {
+ this.groupService.getGroupRole(this.server, this.group.user_group_id)
+ .subscribe((roles: Role[]) => {
+ this.roles = roles;
+ });
+ }
+}
diff --git a/src/app/components/group-details/members-filter.pipe.spec.ts b/src/app/components/group-details/members-filter.pipe.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-details/members-filter.pipe.ts b/src/app/components/group-details/members-filter.pipe.ts
new file mode 100644
index 00000000..570a1456
--- /dev/null
+++ b/src/app/components/group-details/members-filter.pipe.ts
@@ -0,0 +1,32 @@
+/*
+* 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 {User} from "@models/users/user";
+
+@Pipe({
+ name: 'membersFilter'
+})
+export class MembersFilterPipe implements PipeTransform {
+
+ transform(members: User[], filterText: string): User[] {
+ if (!members) {
+ return [];
+ }
+ if (filterText === undefined || filterText === '') {
+ return members;
+ }
+
+ return members.filter((member: User) => member.username.toLowerCase().includes(filterText.toLowerCase()));
+ }
+
+}
diff --git a/src/app/components/group-details/paginator.pipe.spec.ts b/src/app/components/group-details/paginator.pipe.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-details/paginator.pipe.ts b/src/app/components/group-details/paginator.pipe.ts
new file mode 100644
index 00000000..7bc69424
--- /dev/null
+++ b/src/app/components/group-details/paginator.pipe.ts
@@ -0,0 +1,41 @@
+/*
+* 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 {User} from "@models/users/user";
+import {PageEvent} from "@angular/material/paginator";
+
+@Pipe({
+ name: 'paginator'
+})
+export class PaginatorPipe implements PipeTransform {
+
+ transform(elements: T[] | undefined, paginatorEvent: PageEvent | undefined): T[] {
+ if (!elements) {
+ return [];
+ }
+
+ if (!paginatorEvent) {
+ paginatorEvent = {
+ length: elements.length,
+ pageIndex: 0,
+ pageSize: 5
+ };
+ }
+
+
+ return elements.slice(
+ paginatorEvent.pageIndex * paginatorEvent.pageSize,
+ (paginatorEvent.pageIndex + 1) * paginatorEvent.pageSize);
+ }
+
+}
diff --git a/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.html b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.html
new file mode 100644
index 00000000..e5811dad
--- /dev/null
+++ b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.scss b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.scss
new file mode 100644
index 00000000..8ebc2b8a
--- /dev/null
+++ b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.scss
@@ -0,0 +1,20 @@
+:host {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.header {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.button {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+}
diff --git a/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.spec.ts b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.ts b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.ts
new file mode 100644
index 00000000..2e86af2e
--- /dev/null
+++ b/src/app/components/group-details/remove-to-group-dialog/remove-to-group-dialog.component.ts
@@ -0,0 +1,37 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {User} from "@models/users/user";
+
+@Component({
+ selector: 'app-remove-user-to-group-dialog',
+ templateUrl: './remove-to-group-dialog.component.html',
+ styleUrls: ['./remove-to-group-dialog.component.scss']
+})
+export class RemoveToGroupDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { name: string }) { }
+
+ ngOnInit(): void {
+ }
+
+ onCancel() {
+ this.dialogRef.close(false);
+ }
+
+ onConfirm() {
+ this.dialogRef.close(true);
+ }
+}
diff --git a/src/app/components/group-details/remove-user-to-group-dialog/remove-user-to-group-dialog.component.ts b/src/app/components/group-details/remove-user-to-group-dialog/remove-user-to-group-dialog.component.ts
new file mode 100644
index 00000000..ef70a89f
--- /dev/null
+++ b/src/app/components/group-details/remove-user-to-group-dialog/remove-user-to-group-dialog.component.ts
@@ -0,0 +1,37 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {User} from "@models/users/user";
+
+@Component({
+ selector: 'app-remove-user-to-group-dialog',
+ templateUrl: './remove-user-to-group-dialog.component.html',
+ styleUrls: ['./remove-user-to-group-dialog.component.scss']
+})
+export class RemoveUserToGroupDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { user: User }) { }
+
+ ngOnInit(): void {
+ }
+
+ onCancel() {
+ this.dialogRef.close();
+ }
+
+ onConfirm() {
+ this.dialogRef.close(this.data.user);
+ }
+}
diff --git a/src/app/components/group-management/add-group-dialog/GroupNameValidator.ts b/src/app/components/group-management/add-group-dialog/GroupNameValidator.ts
new file mode 100644
index 00000000..09b9cda8
--- /dev/null
+++ b/src/app/components/group-management/add-group-dialog/GroupNameValidator.ts
@@ -0,0 +1,26 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class GroupNameValidator {
+ get(groupName) {
+ const pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/);
+
+ if (!pattern.test(groupName.value)) {
+ return null;
+ }
+
+ return { invalidName: true };
+ }
+}
diff --git a/src/app/components/group-management/add-group-dialog/add-group-dialog.component.html b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.html
new file mode 100644
index 00000000..8bb21d51
--- /dev/null
+++ b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.html
@@ -0,0 +1,57 @@
+Create new group
+
+
+ Add users to group:
+
+ Users
+
+
+
+ {{user.username}} - {{user.email}}
+
+
+
+
+
+
+
+
{{user.username}}
+
{{user.email}}
+
delete
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/group-management/add-group-dialog/add-group-dialog.component.scss b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.scss
new file mode 100644
index 00000000..0ab6fbfb
--- /dev/null
+++ b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.scss
@@ -0,0 +1,25 @@
+.file-name-form-field {
+ width: 100%;
+}
+
+.project-snackbar {
+ background: #2196f3;
+}
+
+.userList {
+ display: flex;
+ margin: 10px;
+ justify-content: space-between;
+ flex: 1 1 auto;
+}
+
+.users {
+ display: flex;
+ height: 180px;
+ overflow: auto;
+ flex-direction: column;
+}
+
+.button-div {
+ float: right;
+}
diff --git a/src/app/components/group-management/add-group-dialog/add-group-dialog.component.spec.ts b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-management/add-group-dialog/add-group-dialog.component.ts b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.ts
new file mode 100644
index 00000000..dc96745e
--- /dev/null
+++ b/src/app/components/group-management/add-group-dialog/add-group-dialog.component.ts
@@ -0,0 +1,137 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
+import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
+import {groupNameAsyncValidator} from "@components/group-management/add-group-dialog/groupNameAsyncValidator";
+import {GroupNameValidator} from "@components/group-management/add-group-dialog/GroupNameValidator";
+import {GroupService} from "../../../services/group.service";
+import {Server} from "../../../models/server";
+import {BehaviorSubject, forkJoin, timer} from "rxjs";
+import {User} from "@models/users/user";
+import {UserService} from "@services/user.service";
+import {ToasterService} from "@services/toaster.service";
+import {PageEvent} from "@angular/material/paginator";
+import {Observable} from "rxjs/Rx";
+import {Group} from "@models/groups/group";
+import {map, startWith} from "rxjs/operators";
+
+@Component({
+ selector: 'app-add-group-dialog',
+ templateUrl: './add-group-dialog.component.html',
+ styleUrls: ['./add-group-dialog.component.scss'],
+ providers: [GroupNameValidator]
+})
+export class AddGroupDialogComponent implements OnInit {
+
+ groupNameForm: FormGroup;
+ server: Server;
+
+ users: User[];
+ usersToAdd: Set = new Set([]);
+ filteredUsers: Observable
+ loading = false;
+ autocompleteControl = new FormControl();
+
+
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { server: Server },
+ private formBuilder: FormBuilder,
+ private groupNameValidator: GroupNameValidator,
+ private groupService: GroupService,
+ private userService: UserService,
+ private toasterService: ToasterService) {
+ }
+
+ ngOnInit(): void {
+ this.server = this.data.server;
+ this.groupNameForm = this.formBuilder.group({
+ groupName: new FormControl(
+ null,
+ [Validators.required, this.groupNameValidator.get],
+ [groupNameAsyncValidator(this.data.server, this.groupService)]
+ ),
+ });
+ this.userService.list(this.server)
+ .subscribe((users: User[]) => {
+ this.users = users;
+ this.filteredUsers = this.autocompleteControl.valueChanges.pipe(
+ startWith(''),
+ map(value => this._filter(value)),
+ );
+ })
+ }
+
+ onKeyDown(event) {
+ if (event.key === 'Enter') {
+ this.onAddClick();
+ }
+ }
+
+ get form() {
+ return this.groupNameForm.controls;
+ }
+
+ onAddClick() {
+ if (this.groupNameForm.invalid) {
+ return;
+ }
+ const groupName = this.groupNameForm.controls['groupName'].value;
+ const toAdd = Array.from(this.usersToAdd.values());
+
+
+ this.groupService.addGroup(this.server, groupName)
+ .subscribe((group) => {
+ toAdd.forEach((user: User) => {
+ this.groupService.addMemberToGroup(this.server, group, user)
+ .subscribe(() => {
+ this.toasterService.success(`user ${user.username} was added`);
+ },
+ (error) => {
+ this.toasterService.error(`An error occur while trying to add user ${user.username} to group ${groupName}`);
+ })
+ })
+ this.dialogRef.close(true);
+ }, (error) => {
+ this.toasterService.error(`An error occur while trying to create new group ${groupName}`);
+ this.dialogRef.close(false);
+ });
+ }
+
+ onNoClick() {
+ this.dialogRef.close();
+ }
+
+ selectedUser(user: User) {
+ this.usersToAdd.add(user);
+ }
+
+ delUser(user: User) {
+ this.usersToAdd.delete(user);
+ }
+
+ private _filter(value: string): User[] {
+ if (typeof value === 'string') {
+ const filterValue = value.toLowerCase();
+
+ return this.users.filter(option => option.username.toLowerCase().includes(filterValue)
+ || (option.email?.toLowerCase().includes(filterValue)));
+ }
+ }
+
+ displayFn(value): string {
+ return value && value.username ? value.username : '';
+ }
+
+}
diff --git a/src/app/components/group-management/add-group-dialog/groupNameAsyncValidator.ts b/src/app/components/group-management/add-group-dialog/groupNameAsyncValidator.ts
new file mode 100644
index 00000000..a511d16c
--- /dev/null
+++ b/src/app/components/group-management/add-group-dialog/groupNameAsyncValidator.ts
@@ -0,0 +1,29 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import { FormControl } from '@angular/forms';
+import { timer } from 'rxjs';
+import {map, switchMap, tap} from 'rxjs/operators';
+import {Server} from "../../../models/server";
+import {GroupService} from "../../../services/group.service";
+
+export const groupNameAsyncValidator = (server: Server, groupService: GroupService) => {
+ return (control: FormControl) => {
+ return timer(500).pipe(
+ switchMap(() => groupService.getGroups(server)),
+ map((response) => {
+ console.log(response);
+ return (response.find((n) => n.name === control.value) ? { projectExist: true } : null);
+ })
+ );
+ };
+};
diff --git a/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.html b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.html
new file mode 100644
index 00000000..4ab22367
--- /dev/null
+++ b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.html
@@ -0,0 +1,8 @@
+Are you sure to delete group named:
+{{group.name}}
+
+
+
+
diff --git a/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.scss b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.scss
new file mode 100644
index 00000000..1b0fdabd
--- /dev/null
+++ b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.scss
@@ -0,0 +1,6 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.spec.ts b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.ts b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.ts
new file mode 100644
index 00000000..b4af6d15
--- /dev/null
+++ b/src/app/components/group-management/delete-group-dialog/delete-group-dialog.component.ts
@@ -0,0 +1,37 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {Group} from "@models/groups/group";
+
+@Component({
+ selector: 'app-delete-group-dialog',
+ templateUrl: './delete-group-dialog.component.html',
+ styleUrls: ['./delete-group-dialog.component.scss']
+})
+export class DeleteGroupDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { groups: Group[] }) { }
+
+ ngOnInit(): void {
+ }
+
+ onCancel() {
+ this.dialogRef.close();
+ }
+
+ onDelete() {
+ this.dialogRef.close(true);
+ }
+}
diff --git a/src/app/components/group-management/group-management.component.html b/src/app/components/group-management/group-management.component.html
new file mode 100644
index 00000000..587d07e3
--- /dev/null
+++ b/src/app/components/group-management/group-management.component.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ Name |
+ {{element.name}} |
+
+
+
+ Creation date |
+ {{element.created_at}} |
+
+
+
+
+ Last update |
+ {{element.updated_at}} |
+
+
+
+
+ is build in |
+ {{element.is_builtin}} |
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/group-management/group-management.component.scss b/src/app/components/group-management/group-management.component.scss
new file mode 100644
index 00000000..2b5547ab
--- /dev/null
+++ b/src/app/components/group-management/group-management.component.scss
@@ -0,0 +1,26 @@
+table {
+ width: 100%;
+}
+
+.full-width {
+ width: 940px;
+ margin-left: -470px;
+ left: 50%;
+}
+
+.add-group-button {
+ height: 40px;
+ width: 160px;
+ margin: 20px;
+}
+
+.loader {
+ position: absolute;
+ margin: auto;
+ height: 175px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ width: 175px;
+}
diff --git a/src/app/components/group-management/group-management.component.spec.ts b/src/app/components/group-management/group-management.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/group-management/group-management.component.ts b/src/app/components/group-management/group-management.component.ts
new file mode 100644
index 00000000..47424cdb
--- /dev/null
+++ b/src/app/components/group-management/group-management.component.ts
@@ -0,0 +1,133 @@
+/*
+* 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, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
+import {ActivatedRoute, Router} from "@angular/router";
+import {ServerService} from "../../services/server.service";
+import {ToasterService} from "../../services/toaster.service";
+import {GroupService} from "../../services/group.service";
+import {Server} from "../../models/server";
+import {Group} from "../../models/groups/group";
+import {MatSort, Sort} from "@angular/material/sort";
+import {MatDialog} from "@angular/material/dialog";
+import {AddGroupDialogComponent} from "@components/group-management/add-group-dialog/add-group-dialog.component";
+import {DeleteGroupDialogComponent} from "@components/group-management/delete-group-dialog/delete-group-dialog.component";
+import {SelectionModel} from "@angular/cdk/collections";
+import {forkJoin} from "rxjs";
+import {MatPaginator} from "@angular/material/paginator";
+import {MatTableDataSource} from "@angular/material/table";
+
+
+@Component({
+ selector: 'app-group-management',
+ templateUrl: './group-management.component.html',
+ styleUrls: ['./group-management.component.scss']
+})
+export class GroupManagementComponent implements OnInit {
+ server: Server;
+
+ @ViewChildren('groupsPaginator') groupsPaginator: QueryList;
+ @ViewChildren('groupsSort') groupsSort: QueryList;
+
+ public displayedColumns = ['select', 'name', 'created_at', 'updated_at', 'is_builtin', 'delete'];
+ selection = new SelectionModel(true, []);
+ groups: Group[];
+ dataSource = new MatTableDataSource();
+ searchText: string;
+ isReady = false;
+
+ constructor(
+ private route: ActivatedRoute,
+ private serverService: ServerService,
+ private toasterService: ToasterService,
+ public groupService: GroupService,
+ public dialog: MatDialog
+ ) {
+ }
+
+
+ ngOnInit(): void {
+ const serverId = this.route.parent.snapshot.paramMap.get('server_id');
+ this.serverService.get(+serverId).then((server: Server) => {
+ this.server = server;
+ this.refresh();
+ });
+ }
+
+ ngAfterViewInit() {
+ this.groupsPaginator.changes.subscribe((comps: QueryList ) =>
+ {
+ this.dataSource.paginator = comps.first;
+ });
+ this.groupsSort.changes.subscribe((comps: QueryList) => {
+ this.dataSource.sort = comps.first;
+ });
+ this.dataSource.sortingDataAccessor = (item, property) => {
+ switch (property) {
+ case 'name':
+ return item[property] ? item[property].toLowerCase() : '';
+ default:
+ return item[property];
+ }
+ };
+ }
+
+ isAllSelected() {
+ const numSelected = this.selection.selected.length;
+ const numRows = this.groups.length;
+ return numSelected === numRows;
+ }
+
+ masterToggle() {
+ this.isAllSelected() ?
+ this.selection.clear() :
+ this.groups.forEach(row => this.selection.select(row));
+ }
+
+ addGroup() {
+ this.dialog
+ .open(AddGroupDialogComponent, {width: '600px', height: '500px', data: {server: this.server}})
+ .afterClosed()
+ .subscribe((added: boolean) => {
+ if (added) {
+ this.refresh();
+ }
+ });
+ }
+
+ refresh() {
+ this.groupService.getGroups(this.server).subscribe((groups: Group[]) => {
+ this.isReady = true;
+ this.groups = groups;
+ this.dataSource.data = groups;
+ this.selection.clear();
+ });
+ }
+
+ onDelete(groupsToDelete: Group[]) {
+ this.dialog
+ .open(DeleteGroupDialogComponent, {width: '500px', height: '250px', data: {groups: groupsToDelete}})
+ .afterClosed()
+ .subscribe((isDeletedConfirm) => {
+ if (isDeletedConfirm) {
+ const observables = groupsToDelete.map((group: Group) => this.groupService.delete(this.server, group.user_group_id));
+ forkJoin(observables)
+ .subscribe(() => {
+ this.refresh();
+ },
+ (error) => {
+ this.toasterService.error(`An error occur while trying to delete group`);
+ });
+ }
+ });
+ }
+}
diff --git a/src/app/components/management/management.component.html b/src/app/components/management/management.component.html
new file mode 100644
index 00000000..24f7e2ed
--- /dev/null
+++ b/src/app/components/management/management.component.html
@@ -0,0 +1,9 @@
+
+
diff --git a/src/app/components/management/management.component.scss b/src/app/components/management/management.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/management/management.component.spec.ts b/src/app/components/management/management.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/management/management.component.ts b/src/app/components/management/management.component.ts
new file mode 100644
index 00000000..12273a93
--- /dev/null
+++ b/src/app/components/management/management.component.ts
@@ -0,0 +1,40 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import { Component, OnInit } from '@angular/core';
+import {ActivatedRoute, Router} from "@angular/router";
+import {Server} from "@models/server";
+import {ServerService} from "@services/server.service";
+
+@Component({
+ selector: 'app-management',
+ templateUrl: './management.component.html',
+ styleUrls: ['./management.component.scss']
+})
+export class ManagementComponent implements OnInit {
+
+ server: Server;
+ links = ['users', 'groups', 'roles', 'permissions'];
+ activeLink: string = this.links[0];
+
+ constructor(
+ private route: ActivatedRoute,
+ public router: Router,
+ private serverService: ServerService) { }
+
+ ngOnInit(): void {
+ const serverId = this.route.snapshot.paramMap.get('server_id');
+ this.serverService.get(+serverId).then((server: Server) => {
+ this.server = server;
+ });
+ }
+}
diff --git a/src/app/components/permissions-management/action-button/action-button.component.html b/src/app/components/permissions-management/action-button/action-button.component.html
new file mode 100644
index 00000000..cac08740
--- /dev/null
+++ b/src/app/components/permissions-management/action-button/action-button.component.html
@@ -0,0 +1,6 @@
+
diff --git a/src/app/components/permissions-management/action-button/action-button.component.scss b/src/app/components/permissions-management/action-button/action-button.component.scss
new file mode 100644
index 00000000..fe2111dc
--- /dev/null
+++ b/src/app/components/permissions-management/action-button/action-button.component.scss
@@ -0,0 +1,8 @@
+.allow {
+ background-color: green;
+ border-radius: unset !important;
+}
+
+.deny {
+ background-color: darkred;
+}
diff --git a/src/app/components/permissions-management/action-button/action-button.component.spec.ts b/src/app/components/permissions-management/action-button/action-button.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/permissions-management/action-button/action-button.component.ts b/src/app/components/permissions-management/action-button/action-button.component.ts
new file mode 100644
index 00000000..9cf23619
--- /dev/null
+++ b/src/app/components/permissions-management/action-button/action-button.component.ts
@@ -0,0 +1,38 @@
+/*
+* 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();
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+ change() {
+ this.action === PermissionActions.DENY ? this.action = PermissionActions.ALLOW : this.action = PermissionActions.DENY;
+ this.update.emit(this.action);
+ }
+}
diff --git a/src/app/components/permissions-management/add-permission-line/add-permission-line.component.html b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.html
new file mode 100644
index 00000000..219ebc2a
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/permissions-management/add-permission-line/add-permission-line.component.scss b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.scss
new file mode 100644
index 00000000..aed624da
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.scss
@@ -0,0 +1,49 @@
+.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;
+}
diff --git a/src/app/components/permissions-management/add-permission-line/add-permission-line.component.spec.ts b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.spec.ts
new file mode 100644
index 00000000..f4e537a7
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.spec.ts
@@ -0,0 +1,132 @@
+/* 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 {Server} from "@models/server";
+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 = (server: Server, permission: Permission): Observable => {
+ 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 = (server: Server, permission: Permission): Observable => {
+ const error = new HttpErrorResponse({
+ error: new Error("An error occur"),
+ headers: undefined,
+ status: 500,
+ statusText: 'error from server'
+ });
+ return throwError(error);
+ };
+
+ toasterService.error = (message: string) => {
+ errorMessage = message;
+ };
+
+ comp.save();
+ tick();
+ expect(errorMessage).toBeTruthy();
+ }));
+});
diff --git a/src/app/components/permissions-management/add-permission-line/add-permission-line.component.ts b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.ts
new file mode 100644
index 00000000..aeeffbc2
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/add-permission-line.component.ts
@@ -0,0 +1,82 @@
+/*
+* 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 {Server} from "@models/server";
+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() server: Server;
+ @Output() addPermissionEvent = new EventEmitter();
+ 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.server, this.permission)
+ .subscribe(() => {
+ this.toasterService.success(`permission was created`);
+ this.reset();
+ }, (error: HttpErrorResponse) => {
+ this.toasterService.error(`
+ ${error.message}
+ ${error.error.message}`);
+ });
+ }
+}
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/PermissionPath.spec.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/PermissionPath.spec.ts
new file mode 100644
index 00000000..9c1f35fb
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/PermissionPath.spec.ts
@@ -0,0 +1,82 @@
+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();
+ });
+
+});
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/PermissionPath.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/PermissionPath.ts
new file mode 100644
index 00000000..5e84941b
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/PermissionPath.ts
@@ -0,0 +1,56 @@
+/*
+* 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);
+ }
+}
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/SubPath.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/SubPath.ts
new file mode 100644
index 00000000..11fe2675
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/SubPath.ts
@@ -0,0 +1,24 @@
+/*
+* 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
+*/
+export class SubPath {
+
+ /**
+ * @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) {
+ }
+}
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe.spec.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe.spec.ts
new file mode 100644
index 00000000..fb5e7afa
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe.spec.ts
@@ -0,0 +1,37 @@
+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);
+ });
+});
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe.ts
new file mode 100644
index 00000000..9fcf0be0
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/filter-complete.pipe.ts
@@ -0,0 +1,32 @@
+/*
+* 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);
+ });
+ }
+
+}
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.html b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.html
new file mode 100644
index 00000000..b4297c5e
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.html
@@ -0,0 +1,37 @@
+
+
Path: /
+
{{p}}/
+
+
+
+
+ {{value}}
+
+
+
+
+
+ *
+
+ {{data.name}}
+
+
+
+
+
+ cancel
+ add_circle_outline
+ check_circle
+
+
+
+
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.scss b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.scss
new file mode 100644
index 00000000..2720f01e
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.scss
@@ -0,0 +1,22 @@
+.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;
+}
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.spec.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.ts b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.ts
new file mode 100644
index 00000000..2b145c5c
--- /dev/null
+++ b/src/app/components/permissions-management/add-permission-line/path-auto-complete/path-auto-complete.component.ts
@@ -0,0 +1,98 @@
+/*
+* 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 {Server} from "@models/server";
+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();
+ @Input() server: Server;
+ 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.server, 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;
+ }
+}
diff --git a/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.html b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.html
new file mode 100644
index 00000000..1983ebb8
--- /dev/null
+++ b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.html
@@ -0,0 +1,12 @@
+
+
confirm deleting permission:
+
{{data.permission_id}}
+
{{data.path}}
+
{{data.methods.join(',')}}
+
{{data.action}}
+
{{data.description}}
+
+
+
+
+
diff --git a/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.scss b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.scss
new file mode 100644
index 00000000..51a31794
--- /dev/null
+++ b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.scss
@@ -0,0 +1,18 @@
+.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;
+}
diff --git a/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.spec.ts b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.ts b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.ts
new file mode 100644
index 00000000..cc1a149c
--- /dev/null
+++ b/src/app/components/permissions-management/delete-permission-dialog/delete-permission-dialog.component.ts
@@ -0,0 +1,37 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {Permission} from "@models/api/permission";
+
+@Component({
+ selector: 'app-delete-permission-dialog',
+ templateUrl: './delete-permission-dialog.component.html',
+ styleUrls: ['./delete-permission-dialog.component.scss']
+})
+export class DeletePermissionDialogComponent implements OnInit {
+
+ constructor(private dialog: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: Permission) { }
+
+ ngOnInit(): void {
+ }
+
+ cancel() {
+ this.dialog.close(false);
+ }
+
+ confirm() {
+ this.dialog.close(true);
+ }
+}
diff --git a/src/app/components/permissions-management/display-path.pipe.spec.ts b/src/app/components/permissions-management/display-path.pipe.spec.ts
new file mode 100644
index 00000000..6edd557d
--- /dev/null
+++ b/src/app/components/permissions-management/display-path.pipe.spec.ts
@@ -0,0 +1,57 @@
+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 {Server} from "@models/server";
+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 = (server: Server, key: string, value?: string, extraParams?: IExtraParams[]): Observable => {
+ 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 server = new Server();
+ comp
+ .transform('/project/1111-2222-3333/nodes/2222-2222-2222', server)
+ .subscribe((res: string) => {
+ result = res;
+ });
+
+ tick();
+ expect(result).toEqual('/project/myProject/nodes/node1');
+ }));
+});
diff --git a/src/app/components/permissions-management/display-path.pipe.ts b/src/app/components/permissions-management/display-path.pipe.ts
new file mode 100644
index 00000000..49260340
--- /dev/null
+++ b/src/app/components/permissions-management/display-path.pipe.ts
@@ -0,0 +1,54 @@
+/*
+* 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 {Server} from "@models/server";
+
+@Pipe({
+ name: 'displayPath'
+})
+export class DisplayPathPipe implements PipeTransform {
+
+ constructor(private apiInformation: ApiInformationService) {
+ }
+
+ transform(originalPath: string, server: Server): Observable {
+ if (!server) {
+ 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(server, 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;
+ })
+ );
+ }
+
+}
diff --git a/src/app/components/permissions-management/method-button/method-button.component.html b/src/app/components/permissions-management/method-button/method-button.component.html
new file mode 100644
index 00000000..4e4a10cc
--- /dev/null
+++ b/src/app/components/permissions-management/method-button/method-button.component.html
@@ -0,0 +1,7 @@
+
diff --git a/src/app/components/permissions-management/method-button/method-button.component.scss b/src/app/components/permissions-management/method-button/method-button.component.scss
new file mode 100644
index 00000000..cc1b1778
--- /dev/null
+++ b/src/app/components/permissions-management/method-button/method-button.component.scss
@@ -0,0 +1,10 @@
+:host {
+ padding: unset !important;
+}
+
+.enable {
+ color: green !important;
+}
+.disabled {
+ color: dimgrey;
+}
diff --git a/src/app/components/permissions-management/method-button/method-button.component.spec.ts b/src/app/components/permissions-management/method-button/method-button.component.spec.ts
new file mode 100644
index 00000000..087ced03
--- /dev/null
+++ b/src/app/components/permissions-management/method-button/method-button.component.spec.ts
@@ -0,0 +1,53 @@
+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();
+
+ }));
+
+
+});
diff --git a/src/app/components/permissions-management/method-button/method-button.component.ts b/src/app/components/permissions-management/method-button/method-button.component.ts
new file mode 100644
index 00000000..b9a2c373
--- /dev/null
+++ b/src/app/components/permissions-management/method-button/method-button.component.ts
@@ -0,0 +1,38 @@
+/*
+* 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});
+ }
+}
diff --git a/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.html b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.html
new file mode 100644
index 00000000..72e78ae9
--- /dev/null
+++ b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.html
@@ -0,0 +1,57 @@
+
+
+
+
+ {{permission.path | displayPath: server | async}}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.scss b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.scss
new file mode 100644
index 00000000..8d6e5890
--- /dev/null
+++ b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.scss
@@ -0,0 +1,28 @@
+.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;
+}
+
diff --git a/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.spec.ts b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.ts b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.ts
new file mode 100644
index 00000000..c30839e6
--- /dev/null
+++ b/src/app/components/permissions-management/permission-edit-line/permission-edit-line.component.ts
@@ -0,0 +1,81 @@
+/*
+* 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 {Server} from '@models/server';
+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() server: Server;
+
+ isEditable = false;
+ @Output() update = new EventEmitter();
+
+ constructor(public apiInformation: ApiInformationService,
+ private permissionService: PermissionsService,
+ private toasterService: ToasterService,
+ private dialog: MatDialog) {
+ }
+
+
+ onDelete() {
+ this.dialog.open(DeletePermissionDialogComponent,
+ {width: '700px', height: '500px', data: this.permission})
+ .afterClosed()
+ .subscribe((confirm: boolean) => {
+ if (confirm) {
+ this.permissionService.delete(this.server, 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.server, 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);
+ }
+}
diff --git a/src/app/components/permissions-management/permissions-filter.pipe.spec.ts b/src/app/components/permissions-management/permissions-filter.pipe.spec.ts
new file mode 100644
index 00000000..41802817
--- /dev/null
+++ b/src/app/components/permissions-management/permissions-filter.pipe.spec.ts
@@ -0,0 +1,57 @@
+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);
+ });
+});
diff --git a/src/app/components/permissions-management/permissions-filter.pipe.ts b/src/app/components/permissions-management/permissions-filter.pipe.ts
new file mode 100644
index 00000000..71b4e75f
--- /dev/null
+++ b/src/app/components/permissions-management/permissions-filter.pipe.ts
@@ -0,0 +1,32 @@
+/*
+* 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()));
+ }
+
+}
diff --git a/src/app/components/permissions-management/permissions-management.component.html b/src/app/components/permissions-management/permissions-management.component.html
new file mode 100644
index 00000000..5494188f
--- /dev/null
+++ b/src/app/components/permissions-management/permissions-management.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ {{option.name}}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/permissions-management/permissions-management.component.scss b/src/app/components/permissions-management/permissions-management.component.scss
new file mode 100644
index 00000000..e7b3efa5
--- /dev/null
+++ b/src/app/components/permissions-management/permissions-management.component.scss
@@ -0,0 +1,36 @@
+.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;
+}
diff --git a/src/app/components/permissions-management/permissions-management.component.spec.ts b/src/app/components/permissions-management/permissions-management.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/permissions-management/permissions-management.component.ts b/src/app/components/permissions-management/permissions-management.component.ts
new file mode 100644
index 00000000..f48e44e6
--- /dev/null
+++ b/src/app/components/permissions-management/permissions-management.component.ts
@@ -0,0 +1,80 @@
+/*
+* 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 {Server} from "@models/server";
+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 {ServerService} from "@services/server.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 {
+ server: Server;
+ 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 serverService: ServerService,
+ private apiInformationService: ApiInformationService) { }
+
+ ngOnInit(): void {
+ const serverId = this.route.parent.snapshot.paramMap.get('server_id');
+ this.serverService.get(+serverId).then((server: Server) => {
+ this.server = server;
+ this.refresh();
+ });
+ }
+
+ refresh() {
+ this.permissionService.list(this.server).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);
+ }
+
+}
diff --git a/src/app/components/permissions-management/permissions-type-filter.pipe.ts b/src/app/components/permissions-management/permissions-type-filter.pipe.ts
new file mode 100644
index 00000000..a5ce9fe0
--- /dev/null
+++ b/src/app/components/permissions-management/permissions-type-filter.pipe.ts
@@ -0,0 +1,32 @@
+/*
+* 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()));
+ }
+
+}
diff --git a/src/app/components/role-management/add-role-dialog/add-role-dialog.component.html b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.html
new file mode 100644
index 00000000..d1cd0b6d
--- /dev/null
+++ b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.html
@@ -0,0 +1,30 @@
+Create new role
+
diff --git a/src/app/components/role-management/add-role-dialog/add-role-dialog.component.scss b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.scss
new file mode 100644
index 00000000..d0a286af
--- /dev/null
+++ b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.scss
@@ -0,0 +1,7 @@
+mat-form-field {
+ width: 100%;
+}
+
+.project-snackbar {
+ background: #2196f3;
+}
diff --git a/src/app/components/role-management/add-role-dialog/add-role-dialog.component.spec.ts b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/add-role-dialog/add-role-dialog.component.ts b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.ts
new file mode 100644
index 00000000..58f58311
--- /dev/null
+++ b/src/app/components/role-management/add-role-dialog/add-role-dialog.component.ts
@@ -0,0 +1,60 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {Server} from "@models/server";
+import {GroupNameValidator} from "@components/group-management/add-group-dialog/GroupNameValidator";
+import {GroupService} from "@services/group.service";
+import {groupNameAsyncValidator} from "@components/group-management/add-group-dialog/groupNameAsyncValidator";
+
+@Component({
+ selector: 'app-add-role-dialog',
+ templateUrl: './add-role-dialog.component.html',
+ styleUrls: ['./add-role-dialog.component.scss']
+})
+export class AddRoleDialogComponent implements OnInit {
+
+ roleNameForm: FormGroup;
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { server: Server },
+ private formBuilder: FormBuilder) {
+ }
+
+ ngOnInit(): void {
+ this.roleNameForm = this.formBuilder.group({
+ name: new FormControl(),
+ description: new FormControl()
+ });
+ }
+
+ get form() {
+ return this.roleNameForm.controls;
+ }
+
+ onAddClick() {
+ if (this.roleNameForm.invalid) {
+ return;
+ }
+ const roleName = this.roleNameForm.controls['name'].value;
+ const description = this.roleNameForm.controls['description'].value;
+ this.dialogRef.close({name: roleName, description});
+ }
+
+ onNoClick() {
+ this.dialogRef.close();
+ }
+
+
+}
diff --git a/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.html b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.html
new file mode 100644
index 00000000..5008f0d3
--- /dev/null
+++ b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.html
@@ -0,0 +1,8 @@
+Are you sure to delete role named:
+{{role.name}}
+
+
+
+
diff --git a/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.scss b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.scss
new file mode 100644
index 00000000..1b0fdabd
--- /dev/null
+++ b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.scss
@@ -0,0 +1,6 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.spec.ts b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.ts b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.ts
new file mode 100644
index 00000000..01f41da8
--- /dev/null
+++ b/src/app/components/role-management/delete-role-dialog/delete-role-dialog.component.ts
@@ -0,0 +1,39 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {Role} from "@models/api/role";
+
+@Component({
+ selector: 'app-delete-role-dialog',
+ templateUrl: './delete-role-dialog.component.html',
+ styleUrls: ['./delete-role-dialog.component.scss']
+})
+export class DeleteRoleDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { roles: Role[] }) { }
+
+ ngOnInit(): void {
+ }
+
+ onCancel() {
+ this.dialogRef.close();
+ }
+
+ onDelete() {
+ this.dialogRef.close(true);
+ }
+
+
+}
diff --git a/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.html b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.html
new file mode 100644
index 00000000..b22e793d
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.html
@@ -0,0 +1,15 @@
+
+
+
+
{{permission.methods.join(",")}}
+
{{permission.path | displayPath: server | async}}
+
+
+
+
diff --git a/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.scss b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.scss
new file mode 100644
index 00000000..07167210
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.scss
@@ -0,0 +1,42 @@
+.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;
+}
diff --git a/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.spec.ts b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.ts b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.ts
new file mode 100644
index 00000000..f5cde171
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/editable-permission/editable-permission.component.ts
@@ -0,0 +1,45 @@
+/*
+* 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 { Server } from '@models/server';
+
+@Component({
+ selector: 'app-editable-permission',
+ templateUrl: './editable-permission.component.html',
+ styleUrls: ['./editable-permission.component.scss']
+})
+export class EditablePermissionComponent implements OnInit {
+
+ @Input() permission: Permission;
+ @Input() server: Server;
+ @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}
+ `;
+ }
+}
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.html b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.html
new file mode 100644
index 00000000..ff0fe79f
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
0 || data.remove.length > 0; else nothingTodo">
+
+
0">Permission to Add:
+
+
0">Permission to Remove:
+
+
+
+
+ No change
+
+
+
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.scss b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.scss
new file mode 100644
index 00000000..732815d5
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.scss
@@ -0,0 +1,33 @@
+.change {
+ height: 350px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow-y: auto;
+}
+
+.change div {
+ justify-content: center;
+ justify-items: center;
+ text-align: center;
+}
+
+.title {
+ font-size: 20px;
+}
+
+.button {
+ position: relative;
+ top: 400px;
+ z-index: 1;
+}
+
+.button button {
+ margin-right: 50px;
+}
+
+.noChange {
+ display: flex;
+ justify-content: center;
+ justify-items: center;
+}
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.spec.ts b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.ts b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.ts
new file mode 100644
index 00000000..b1a7dd9f
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component.ts
@@ -0,0 +1,38 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {Group} from "@models/groups/group";
+import {Permission} from "@models/api/permission";
+
+@Component({
+ selector: 'app-permission-editor-validate-dialog',
+ templateUrl: './permission-editor-validate-dialog.component.html',
+ styleUrls: ['./permission-editor-validate-dialog.component.scss']
+})
+export class PermissionEditorValidateDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { add: Permission[], remove: Permission[] }) { }
+
+ ngOnInit(): void {
+ }
+
+ close() {
+ this.dialogRef.close(false);
+ }
+
+ update() {
+ this.dialogRef.close(true);
+ }
+}
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.html b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.html
new file mode 100644
index 00000000..fc746656
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.html
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.scss b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.scss
new file mode 100644
index 00000000..0b325c21
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.scss
@@ -0,0 +1,54 @@
+.editor {
+ display: flex;
+ justify-content: stretch;
+}
+.column {
+ width: 50vw;
+}
+
+.header {
+ margin: 10px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ font-size: 20px;
+}
+
+.header > div > button {
+ margin-right: 30px;
+}
+
+.header > div {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 20px;
+
+}
+
+.title {
+ font-size: 20px;
+ margin-left: 20px;
+}
+.box {
+ width: 50px;
+ height: 25px;
+ border: 1px solid;
+ margin-right: 20px;
+ margin-left: 10px;
+}
+
+.allow {
+ background-color: rgba(5, 76, 5, 0.38);
+}
+
+.deny {
+ background-color: rgba(130, 8, 8, 0.36);
+}
+
+
+.permission-filter {
+ border-bottom: 1px solid;
+ margin: 5px;
+ border-bottom-color: #b0bec5;
+}
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.spec.ts b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.ts b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.ts
new file mode 100644
index 00000000..ffbe254a
--- /dev/null
+++ b/src/app/components/role-management/role-detail/permission-editor/permission-editor.component.ts
@@ -0,0 +1,114 @@
+/*
+* 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, Input, OnInit, Output, EventEmitter} from '@angular/core';
+import {Server} from "@models/server";
+import {Permission} from "@models/api/permission";
+import {MatDialog} from "@angular/material/dialog";
+import {PermissionEditorValidateDialogComponent} from "@components/role-management/role-detail/permission-editor/permission-editor-validate-dialog/permission-editor-validate-dialog.component";
+import {ApiInformationService } from "@services/ApiInformation/api-information.service";
+import {PageEvent} from "@angular/material/paginator";
+import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject";
+
+@Component({
+ selector: 'app-permission-editor',
+ templateUrl: './permission-editor.component.html',
+ styleUrls: ['./permission-editor.component.scss']
+})
+export class PermissionEditorComponent implements OnInit {
+
+ owned: Set;
+ available: Set;
+ searchPermissions: any;
+ filteredOptions: IGenericApiObject[];
+ pageEventOwned: PageEvent | undefined;
+ pageEventAvailable: PageEvent | undefined;
+
+ @Input() server: Server;
+ @Input() ownedPermissions: Permission[];
+ @Input() availablePermissions: Permission[];
+ @Output() updatedPermissions: EventEmitter = new EventEmitter();
+
+
+ constructor(private dialog: MatDialog,
+ private apiInformationService: ApiInformationService) {}
+
+ ngOnInit(): void {
+ this.reset();
+ }
+
+ add(permission: Permission) {
+ this.available.delete(permission);
+ this.owned.add(permission);
+ }
+
+ remove(permission: Permission) {
+ this.owned.delete(permission);
+ this.available.add(permission);
+ }
+
+ reset() {
+ const ownedPermissionId = this.ownedPermissions.map(p => p.permission_id);
+ this.owned = new Set(this.ownedPermissions);
+ this.available = new Set(this.availablePermissions.filter(p => !ownedPermissionId.includes(p.permission_id)));
+ }
+
+ update() {
+ const {add, remove} = this.diff();
+ this.dialog
+ .open(PermissionEditorValidateDialogComponent,
+ {width: '700px', height: '500px', data: {add, remove}})
+ .afterClosed()
+ .subscribe((confirmed: boolean) => {
+ if (confirmed) {
+ this.updatedPermissions.emit({add, remove});
+ }
+
+ });
+ }
+
+ private diff() {
+ const add: Permission[] = [];
+
+ const currentRolePermissionId = this.ownedPermissions.map(p => p.permission_id);
+ this.owned.forEach((permission: Permission) => {
+ if (!currentRolePermissionId.includes(permission.permission_id)) {
+ add.push(permission);
+ }
+ });
+
+ const remove: Permission[] = [];
+ this.ownedPermissions.forEach((permission: Permission) => {
+ if (!this.owned.has(permission)) {
+ remove.push(permission);
+ }
+ });
+
+ return {add, remove};
+ }
+
+ displayFn(value): string {
+ return value && value.name ? value.name : '';
+ }
+
+ changeAutocomplete(inputText) {
+ this.filteredOptions = this.apiInformationService.getIdByObjNameFromCache(inputText);
+ }
+
+ get ownedArray() {
+ return Array.from(this.owned.values());
+ }
+
+ get availableArray() {
+ return Array.from(this.available.values());
+ }
+}
diff --git a/src/app/components/role-management/role-detail/role-detail.component.html b/src/app/components/role-management/role-detail/role-detail.component.html
new file mode 100644
index 00000000..81c52490
--- /dev/null
+++ b/src/app/components/role-management/role-detail/role-detail.component.html
@@ -0,0 +1,60 @@
+
+
+
diff --git a/src/app/components/role-management/role-detail/role-detail.component.scss b/src/app/components/role-management/role-detail/role-detail.component.scss
new file mode 100644
index 00000000..bf38d212
--- /dev/null
+++ b/src/app/components/role-management/role-detail/role-detail.component.scss
@@ -0,0 +1,40 @@
+.main {
+ display: flex;
+ justify-content: space-around;
+}
+
+.details {
+ width: 30vw;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.permissions {
+ width: 35vw;
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+}
+
+.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;
+
+}
+.header > div {
+ font-size: 2em;
+}
diff --git a/src/app/components/role-management/role-detail/role-detail.component.spec.ts b/src/app/components/role-management/role-detail/role-detail.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-detail/role-detail.component.ts b/src/app/components/role-management/role-detail/role-detail.component.ts
new file mode 100644
index 00000000..0ccb142a
--- /dev/null
+++ b/src/app/components/role-management/role-detail/role-detail.component.ts
@@ -0,0 +1,61 @@
+/*
+* 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, OnInit} from '@angular/core';
+import {RoleService} from "@services/role.service";
+import {ActivatedRoute} from "@angular/router";
+import {Server} from "@models/server";
+import {ServerService} from "@services/server.service";
+import {Role} from "@models/api/role";
+import {FormControl, FormGroup} from "@angular/forms";
+import {ToasterService} from "@services/toaster.service";
+import {HttpErrorResponse} from "@angular/common/http";
+
+@Component({
+ selector: 'app-role-detail',
+ templateUrl: './role-detail.component.html',
+ styleUrls: ['./role-detail.component.scss']
+})
+export class RoleDetailComponent implements OnInit {
+ server: Server;
+ role: Role;
+ editRoleForm: FormGroup;
+
+ constructor(private roleService: RoleService,
+ private serverService: ServerService,
+ private toastService: ToasterService,
+ private route: ActivatedRoute) {
+
+ this.editRoleForm = new FormGroup({
+ rolename: new FormControl(),
+ description: new FormControl(),
+ });
+ }
+
+ ngOnInit(): void {
+ this.route.data.subscribe((d: { server: Server; role: Role }) => {
+ this.server = d.server;
+ this.role = d.role;
+ });
+ }
+
+ onUpdate() {
+ this.roleService.update(this.server, this.role)
+ .subscribe(() => {
+ this.toastService.success(`role: ${this.role.name} was updated`);
+ },
+ (error: HttpErrorResponse) => {
+ this.toastService.error(`${error.message}
+ ${error.error.message}`);
+ });
+ }
+}
diff --git a/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.html b/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.html
new file mode 100644
index 00000000..a8a5db30
--- /dev/null
+++ b/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.html
@@ -0,0 +1,16 @@
+
+
diff --git a/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.scss b/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.spec.ts b/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.ts b/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.ts
new file mode 100644
index 00000000..76c8ee5d
--- /dev/null
+++ b/src/app/components/role-management/role-detail/role-permissions/role-permissions.component.ts
@@ -0,0 +1,70 @@
+/*
+* 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, OnInit } from '@angular/core';
+import {ActivatedRoute, Router} from "@angular/router";
+import {MatDialog} from "@angular/material/dialog";
+import {ToasterService} from "@services/toaster.service";
+import {RoleService} from "@services/role.service";
+import {Server} from "@models/server";
+import {Role} from "@models/api/role";
+import {Permission} from "@models/api/permission";
+import {Observable} from "rxjs/Rx";
+import {forkJoin} from "rxjs";
+import {HttpErrorResponse} from "@angular/common/http";
+
+@Component({
+ selector: 'app-role-permissions',
+ templateUrl: './role-permissions.component.html',
+ styleUrls: ['./role-permissions.component.scss']
+})
+export class RolePermissionsComponent implements OnInit {
+ server: Server;
+ role: Role;
+ permissions: Permission[];
+
+ constructor(private route: ActivatedRoute,
+ private dialog: MatDialog,
+ private toastService: ToasterService,
+ private router: Router,
+ private roleService: RoleService) {
+ this.route.data.subscribe((data: { server: Server, role: Role, permissions: Permission[] }) => {
+ this.server = data.server;
+ this.role = data.role;
+ this.permissions = data.permissions;
+ });
+ }
+
+ ngOnInit(): void {
+ }
+
+ updatePermissions(toUpdate) {
+ const {add, remove} = toUpdate;
+ const obs: Observable[] = [];
+ add.forEach((permission: Permission) => {
+ obs.push(this.roleService.addPermission(this.server, this.role, permission));
+ });
+ remove.forEach((permission: Permission) => {
+ obs.push(this.roleService.removePermission(this.server, this.role, permission));
+ });
+
+ forkJoin(obs)
+ .subscribe(() => {
+ this.toastService.success(`permissions updated`);
+ this.router.navigate(['/server', this.server.id, 'management', 'roles', this.role.role_id]);
+ },
+ (error: HttpErrorResponse) => {
+ this.toastService.error(`${error.message}
+ ${error.error.message}`);
+ });
+ }
+}
diff --git a/src/app/components/role-management/role-filter.pipe.spec.ts b/src/app/components/role-management/role-filter.pipe.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-filter.pipe.ts b/src/app/components/role-management/role-filter.pipe.ts
new file mode 100644
index 00000000..f9e70fcb
--- /dev/null
+++ b/src/app/components/role-management/role-filter.pipe.ts
@@ -0,0 +1,34 @@
+/*
+* 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 {Role} from "@models/api/role";
+import {User} from "@models/users/user";
+import {MatTableDataSource} from "@angular/material/table";
+
+@Pipe({
+ name: 'roleFilter'
+})
+export class RoleFilterPipe implements PipeTransform {
+
+ transform(roles: MatTableDataSource, searchText: string): MatTableDataSource {
+ if (!searchText) {
+ return roles;
+ }
+ searchText = searchText.trim().toLowerCase();
+ roles.filter = searchText;
+ return roles;
+
+
+ }
+
+}
diff --git a/src/app/components/role-management/role-management.component.html b/src/app/components/role-management/role-management.component.html
new file mode 100644
index 00000000..b3f5d945
--- /dev/null
+++ b/src/app/components/role-management/role-management.component.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+ {{ row.name }}
+
+
+
+ Description
+
+ {{ row.description }}
+
+
+
+ Permissions (Allow)
+
+
+
+
{{permission.action}}
+
{{permission.methods.join(',')}}
+
{{permission.path}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/role-management/role-management.component.scss b/src/app/components/role-management/role-management.component.scss
new file mode 100644
index 00000000..2045941d
--- /dev/null
+++ b/src/app/components/role-management/role-management.component.scss
@@ -0,0 +1,56 @@
+.add-button {
+ height: 40px;
+ width: 160px;
+ margin: 20px;
+}
+
+.full-width {
+ width: 940px;
+ margin-left: -470px;
+ left: 50%;
+}
+
+.small-col {
+ flex-grow: 0.3;
+}
+
+.active-col {
+ flex-grow: 0.5;
+}
+
+.overflow-col {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ padding-right: 5px;
+}
+
+.permission {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+.permission > div {
+ margin-right: 20px;
+ font-size: 10px;
+}
+.custom-tooltip {
+ font-size:100px;
+}
+
+.loader {
+ position: absolute;
+ margin: auto;
+ height: 175px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ width: 175px;
+}
+
+.permissions-list {
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+}
diff --git a/src/app/components/role-management/role-management.component.spec.ts b/src/app/components/role-management/role-management.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/role-management/role-management.component.ts b/src/app/components/role-management/role-management.component.ts
new file mode 100644
index 00000000..21840342
--- /dev/null
+++ b/src/app/components/role-management/role-management.component.ts
@@ -0,0 +1,149 @@
+/*
+* 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, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
+import {Server} from "@models/server";
+import {MatTableDataSource} from "@angular/material/table";
+import {SelectionModel} from "@angular/cdk/collections";
+import {MatPaginator} from "@angular/material/paginator";
+import {MatSort} from "@angular/material/sort";
+import {ActivatedRoute, Router} from "@angular/router";
+import {ProgressService} from "../../common/progress/progress.service";
+import {ServerService} from "@services/server.service";
+import {MatDialog} from "@angular/material/dialog";
+import {ToasterService} from "@services/toaster.service";
+import {Role} from "@models/api/role";
+import {RoleService} from "@services/role.service";
+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 {forkJoin} from "rxjs";
+import {HttpErrorResponse} from "@angular/common/http";
+
+
+@Component({
+ selector: 'app-role-management',
+ templateUrl: './role-management.component.html',
+ styleUrls: ['./role-management.component.scss']
+})
+export class RoleManagementComponent implements OnInit {
+ server: Server;
+ dataSource = new MatTableDataSource();
+ displayedColumns = ['select', 'name', 'description', 'permissions', 'delete'];
+ selection = new SelectionModel(true, []);
+ searchText = '';
+
+ @ViewChildren('rolesPaginator') rolesPaginator: QueryList;
+ @ViewChildren('rolesSort') rolesSort: QueryList;
+ isReady = false;
+
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private roleService: RoleService,
+ private progressService: ProgressService,
+ private serverService: ServerService,
+ public dialog: MatDialog,
+ private toasterService: ToasterService) {
+ }
+
+ ngOnInit() {
+ const serverId = this.route.parent.snapshot.paramMap.get('server_id');
+ this.serverService.get(+serverId).then((server: Server) => {
+ this.server = server;
+ this.refresh();
+ });
+
+ }
+
+ ngAfterViewInit() {
+ this.rolesPaginator.changes.subscribe((comps: QueryList ) =>
+ {
+ this.dataSource.paginator = comps.first;
+ });
+ this.rolesSort.changes.subscribe((comps: QueryList) => {
+ this.dataSource.sort = comps.first;
+ });
+ this.dataSource.sortingDataAccessor = (item, property) => {
+ switch (property) {
+ case 'name':
+ return item[property] ? item[property].toLowerCase() : '';
+ default:
+ return item[property];
+ }
+ };
+
+ }
+
+ refresh() {
+ this.roleService.get(this.server).subscribe(
+ (roles: Role[]) => {
+ this.isReady = true;
+ this.dataSource.data = roles;
+ },
+ (error) => {
+ this.progressService.setError(error);
+ }
+ );
+ }
+
+ addRole() {
+ const dialogRef = this.dialog.open(AddRoleDialogComponent, {
+ width: '400px',
+ autoFocus: false,
+ disableClose: true,
+ data: {server: this.server},
+ })
+ .afterClosed()
+ .subscribe((role: { name: string; description: string }) => {
+ if (role) {
+ this.roleService.create(this.server, role)
+ .subscribe(() => {
+ this.toasterService.success(`${role.name} role created`);
+ this.refresh();
+ },
+ (error: HttpErrorResponse) => this.toasterService.error(`${error.message}
+ ${error.error.message}`));
+ }
+ });
+ }
+
+ isAllSelected() {
+ const numSelected = this.selection.selected.length;
+ const numRows = this.dataSource.data.length;
+ return numSelected === numRows;
+ }
+
+ masterToggle() {
+ this.isAllSelected() ?
+ this.selection.clear() :
+ this.dataSource.data.forEach(row => this.selection.select(row));
+ }
+
+ onDelete(rolesToDelete: Role[]) {
+ this.dialog
+ .open(DeleteRoleDialogComponent, {width: '500px', height: '250px', data: {roles: rolesToDelete}})
+ .afterClosed()
+ .subscribe((isDeletedConfirm) => {
+ if (isDeletedConfirm) {
+ const observables = rolesToDelete.map((role: Role) => this.roleService.delete(this.server, role.role_id));
+ forkJoin(observables)
+ .subscribe(() => {
+ this.refresh();
+ },
+ (error) => {
+ this.toasterService.error(`An error occur while trying to delete role`);
+ });
+ }
+ });
+ }
+}
diff --git a/src/app/components/user-management/ConfirmPasswordValidator.ts b/src/app/components/user-management/ConfirmPasswordValidator.ts
new file mode 100644
index 00000000..1e26eac3
--- /dev/null
+++ b/src/app/components/user-management/ConfirmPasswordValidator.ts
@@ -0,0 +1,10 @@
+import {AbstractControl, FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms';
+
+
+export function matchingPassword(control: AbstractControl) : ValidationErrors | null {
+ if (control.get('password').value !== control.get('confirmPassword').value) {
+ control.get('confirmPassword').setErrors({confirmPasswordMatch: true});
+ return;
+ }
+ return;
+}
diff --git a/src/app/components/user-management/add-user-dialog/add-user-dialog.component.html b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.html
new file mode 100644
index 00000000..5b2c0391
--- /dev/null
+++ b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.html
@@ -0,0 +1,70 @@
+Create new user
+
diff --git a/src/app/components/user-management/add-user-dialog/add-user-dialog.component.scss b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.scss
new file mode 100644
index 00000000..41142daf
--- /dev/null
+++ b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.scss
@@ -0,0 +1,22 @@
+.input-field {
+ width: 100%;
+}
+
+
+.button-div {
+ float: right;
+}
+
+.groupList {
+ display: flex;
+ margin: 10px;
+ justify-content: space-between;
+ flex: 1 1 auto
+}
+
+.groups {
+ display: flex;
+ height: 180px;
+ overflow: auto;
+ flex-direction: column;
+}
diff --git a/src/app/components/user-management/add-user-dialog/add-user-dialog.component.spec.ts b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/user-management/add-user-dialog/add-user-dialog.component.ts b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.ts
new file mode 100644
index 00000000..3bf5e6e1
--- /dev/null
+++ b/src/app/components/user-management/add-user-dialog/add-user-dialog.component.ts
@@ -0,0 +1,132 @@
+/*
+* 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, OnInit } from '@angular/core';
+import {FormControl, FormGroup, Validators} from "@angular/forms";
+import {MatDialogRef} from "@angular/material/dialog";
+import {UserService} from "@services/user.service";
+import {Server} from "@models/server";
+import {User} from "@models/users/user";
+import {ToasterService} from "@services/toaster.service";
+import {userNameAsyncValidator} from "@components/user-management/add-user-dialog/userNameAsyncValidator";
+import {userEmailAsyncValidator} from "@components/user-management/add-user-dialog/userEmailAsyncValidator";
+import {BehaviorSubject} from "rxjs";
+import {Group} from "@models/groups/group";
+import {GroupService} from "@services/group.service";
+import {Observable} from "rxjs/Rx";
+import {startWith} from "rxjs/operators";
+import {map} from "rxjs//operators";
+import {matchingPassword} from "@components/user-management/ConfirmPasswordValidator";
+
+@Component({
+ selector: 'app-add-user-dialog',
+ templateUrl: './add-user-dialog.component.html',
+ styleUrls: ['./add-user-dialog.component.scss']
+})
+export class AddUserDialogComponent implements OnInit {
+ addUserForm: FormGroup;
+ server: Server;
+
+ groups: Group[];
+ groupsToAdd: Set = new Set([]);
+ autocompleteControl = new FormControl();
+ filteredGroups: Observable;
+
+ constructor(public dialogRef: MatDialogRef,
+ public userService: UserService,
+ private toasterService: ToasterService,
+ private groupService: GroupService) { }
+
+ ngOnInit(): void {
+ this.addUserForm = new FormGroup({
+ username: new FormControl(null, [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.pattern("[a-zA-Z0-9_-]+$")],
+ [userNameAsyncValidator(this.server, this.userService)]),
+ full_name: new FormControl(),
+ email: new FormControl(null,
+ [Validators.email, Validators.required],
+ [userEmailAsyncValidator(this.server, this.userService)]),
+ password: new FormControl(null,
+ [Validators.required, Validators.minLength(6), Validators.maxLength(100)]),
+ confirmPassword: new FormControl(null,
+ [Validators.minLength(6), Validators.maxLength(100), Validators.required] ),
+ is_active: new FormControl(true)
+ },{
+ validators: [matchingPassword]
+ });
+ this.groupService.getGroups(this.server)
+ .subscribe((groups: Group[]) => {
+ this.groups = groups;
+ this.filteredGroups = this.autocompleteControl.valueChanges.pipe(
+ startWith(''),
+ map(value => this._filter(value)),
+ );
+ })
+
+ }
+
+ private _filter(value: string): Group[] {
+ if (typeof value === 'string') {
+ const filterValue = value.toLowerCase();
+
+ return this.groups.filter(option => option.name.toLowerCase().includes(filterValue));
+ }
+
+ }
+
+ get form() {
+ return this.addUserForm.controls;
+ }
+
+ onCancelClick() {
+ this.dialogRef.close();
+ }
+
+ onAddClick() {
+ if (!this.addUserForm.valid) {
+ return;
+ }
+ const newUser = this.addUserForm.value;
+ const toAdd = Array.from(this.groupsToAdd.values());
+ this.userService.add(this.server, newUser)
+ .subscribe((user: User) => {
+ this.toasterService.success(`User ${user.username} added`);
+ toAdd.forEach((group: Group) => {
+ this.groupService.addMemberToGroup(this.server, group, user)
+ .subscribe(() => {
+ this.toasterService.success(`user ${user.username} was added to group ${group.name}`);
+ },
+ (error) => {
+ this.toasterService.error(`An error occur while trying to add user ${user.username} to group ${group.name}`);
+ })
+ })
+ this.dialogRef.close();
+ },
+ (error) => {
+ this.toasterService.error('Cannot create user : ' + error);
+ })
+ }
+
+ deleteGroup(group: Group) {
+ this.groupsToAdd.delete(group);
+ }
+
+ selectedGroup(value: any) {
+ this.groupsToAdd.add(value);
+ }
+
+ displayFn(value): string {
+ return value && value.name ? value.name : '';
+ }
+}
diff --git a/src/app/components/user-management/add-user-dialog/userEmailAsyncValidator.ts b/src/app/components/user-management/add-user-dialog/userEmailAsyncValidator.ts
new file mode 100644
index 00000000..302febae
--- /dev/null
+++ b/src/app/components/user-management/add-user-dialog/userEmailAsyncValidator.ts
@@ -0,0 +1,28 @@
+/*
+* 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 {Server} from "../../../models/server";
+import {UserService} from "../../../services/user.service";
+import {FormControl} from "@angular/forms";
+import {timer} from "rxjs";
+import {map, switchMap} from "rxjs/operators";
+
+export const userEmailAsyncValidator = (server: Server, userService: UserService, except: string = '') => {
+ return (control: FormControl) => {
+ return timer(500).pipe(
+ switchMap(() => userService.list(server)),
+ map((response) => {
+ return (response.find((n) => n.email === control.value && control.value !== except) ? { emailExists: true } : null);
+ })
+ );
+ };
+};
diff --git a/src/app/components/user-management/add-user-dialog/userNameAsyncValidator.ts b/src/app/components/user-management/add-user-dialog/userNameAsyncValidator.ts
new file mode 100644
index 00000000..fb2dd5bb
--- /dev/null
+++ b/src/app/components/user-management/add-user-dialog/userNameAsyncValidator.ts
@@ -0,0 +1,28 @@
+/*
+* 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 {Server} from "../../../models/server";
+import {FormControl} from "@angular/forms";
+import {timer} from "rxjs";
+import {map, switchMap} from "rxjs/operators";
+import {UserService} from "../../../services/user.service";
+
+export const userNameAsyncValidator = (server: Server, userService: UserService, except: string = '') => {
+ return (control: FormControl) => {
+ return timer(500).pipe(
+ switchMap(() => userService.list(server)),
+ map((response) => {
+ return (response.find((n) => n.username === control.value && control.value !== except) ? { userExists: true } : null);
+ })
+ );
+ };
+};
diff --git a/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.html b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.html
new file mode 100644
index 00000000..d15b5b40
--- /dev/null
+++ b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.html
@@ -0,0 +1,10 @@
+Are you sure you want to delete the following users ?
+
+ - {{ user.username }} {{ user.full_name ? '- ' + user.full_name : '' }}
+
+
+
+
+
diff --git a/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.scss b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.scss
new file mode 100644
index 00000000..59c53205
--- /dev/null
+++ b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.scss
@@ -0,0 +1,7 @@
+.button-div {
+ float: right;
+}
+
+ul {
+ list-style-type: none;
+}
diff --git a/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.spec.ts b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.ts b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.ts
new file mode 100644
index 00000000..4b7f931e
--- /dev/null
+++ b/src/app/components/user-management/delete-user-dialog/delete-user-dialog.component.ts
@@ -0,0 +1,38 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {User} from "@models/users/user";
+import {UserService} from "@services/user.service";
+
+@Component({
+ selector: 'app-delete-user-dialog',
+ templateUrl: './delete-user-dialog.component.html',
+ styleUrls: ['./delete-user-dialog.component.scss']
+})
+export class DeleteUserDialogComponent implements OnInit {
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { users: User[] }) { }
+
+ ngOnInit(): void {
+ }
+
+ onCancel() {
+ this.dialogRef.close();
+ }
+
+ onDelete() {
+ this.dialogRef.close(true);
+ }
+}
diff --git a/src/app/components/user-management/edit-user-dialog/edit-user-dialog.component.ts b/src/app/components/user-management/edit-user-dialog/edit-user-dialog.component.ts
new file mode 100644
index 00000000..0a5129d1
--- /dev/null
+++ b/src/app/components/user-management/edit-user-dialog/edit-user-dialog.component.ts
@@ -0,0 +1,81 @@
+/*
+* Software Name : GNS3 Web UI
+* Version: 3
+* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services
+* SPDX-License-Identifier: GPL-3.0-or-later
+*
+* This software is distributed under the GPL-3.0 or any later version,
+* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
+* or see the "LICENSE" file for more details.
+*
+* Author: Sylvain MATHIEU, Elise LEBEAU
+*/
+import {Component, Inject, OnInit} from '@angular/core';
+import {FormControl, FormGroup, Validators} from "@angular/forms";
+import {Server} from "@models/server";
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {UserService} from "@services/user.service";
+import {ToasterService} from "@services/toaster.service";
+import {userNameAsyncValidator} from "@components/user-management/add-user-dialog/userNameAsyncValidator";
+import {userEmailAsyncValidator} from "@components/user-management/add-user-dialog/userEmailAsyncValidator";
+import {User} from "@models/users/user";
+
+@Component({
+ selector: 'app-edit-user-dialog',
+ templateUrl: './edit-user-dialog.component.html',
+ styleUrls: ['./edit-user-dialog.component.scss']
+})
+export class EditUserDialogComponent implements OnInit {
+
+ editUserForm: FormGroup;
+
+ constructor(public dialogRef: MatDialogRef,
+ public userService: UserService,
+ private toasterService: ToasterService,
+ @Inject(MAT_DIALOG_DATA) public data: { user: User, server: Server }) {}
+
+ ngOnInit(): void {
+ this.editUserForm = new FormGroup({
+ username: new FormControl(this.data.user.username, [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.pattern("[a-zA-Z0-9_-]+$")],
+ [userNameAsyncValidator(this.data.server, this.userService, this.data.user.username)]),
+ full_name: new FormControl(this.data.user.full_name),
+ email: new FormControl(this.data.user.email,
+ [Validators.email, Validators.required],
+ [userEmailAsyncValidator(this.data.server, this.userService, this.data.user.email)]),
+ password: new FormControl(null,
+ [Validators.minLength(6), Validators.maxLength(100)]),
+ is_active: new FormControl(this.data.user.is_active)
+ });
+ }
+
+ get form() {
+ return this.editUserForm.controls;
+ }
+
+ onCancelClick() {
+ this.dialogRef.close();
+ }
+
+ onEditClick() {
+ if (!this.editUserForm.valid) {
+ return;
+ }
+ const updatedUser = this.editUserForm.value;
+
+ updatedUser.user_id = this.data.user.user_id;
+ console.log(updatedUser)
+ this.userService.update(this.data.server, updatedUser)
+ .subscribe((user: User) => {
+ console.log("Done ", user)
+ this.toasterService.success(`User ${user.username} updated`);
+ this.dialogRef.close();
+ },
+ (error) => {
+ this.toasterService.error('Cannot update user : ' + error);
+ })
+ }
+
+}
diff --git a/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.html b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.html
new file mode 100644
index 00000000..f61f5fa5
--- /dev/null
+++ b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.html
@@ -0,0 +1,25 @@
+Change password for user :
+
diff --git a/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.scss b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.scss
new file mode 100644
index 00000000..a64b61ba
--- /dev/null
+++ b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.scss
@@ -0,0 +1,7 @@
+.input-field {
+ width: 100%;
+}
+
+.button-div {
+ float: right;
+}
diff --git a/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.spec.ts b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.ts b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.ts
new file mode 100644
index 00000000..192f2644
--- /dev/null
+++ b/src/app/components/user-management/user-detail/change-user-password/change-user-password.component.ts
@@ -0,0 +1,67 @@
+import {Component, Inject, OnInit} from '@angular/core';
+import {AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from "@angular/forms";
+import {User} from "@models/users/user";
+import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
+import {UserService} from "@services/user.service";
+import {Server} from "@models/server";
+import {ToasterService} from "@services/toaster.service";
+import {matchingPassword} from "@components/user-management/ConfirmPasswordValidator";
+
+@Component({
+ selector: 'app-change-user-password',
+ templateUrl: './change-user-password.component.html',
+ styleUrls: ['./change-user-password.component.scss']
+})
+export class ChangeUserPasswordComponent implements OnInit {
+
+ editPasswordForm: FormGroup;
+ user: User;
+
+ constructor(private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: { user: User, server: Server },
+ private userService: UserService,
+ private toasterService: ToasterService) { }
+
+ ngOnInit(): void {
+ this.user = this.data.user;
+ this.editPasswordForm = new FormGroup({
+ password: new FormControl(null,
+ [Validators.minLength(6), Validators.maxLength(100), Validators.required] ),
+ confirmPassword: new FormControl(null,
+ [Validators.minLength(6), Validators.maxLength(100), Validators.required] ),
+ },{
+ validators: [matchingPassword]
+ })
+ }
+
+ get passwordForm() {
+ return this.editPasswordForm.controls;
+ }
+
+
+ onCancel() {
+ this.dialogRef.close();
+ }
+
+ onPasswordSave() {
+ if (!this.editPasswordForm.valid) {
+ return;
+ }
+
+ const updatedUser = {};
+ updatedUser['password'] = this.editPasswordForm.get('password').value;
+ updatedUser['user_id'] = this.user.user_id;
+
+ console.log(updatedUser);
+
+ this.userService.update(this.data.server, updatedUser)
+ .subscribe((user: User) => {
+ this.toasterService.success(`User ${user.username} password updated`);
+ this.editPasswordForm.reset();
+ this.dialogRef.close(true);
+ },
+ (error) => {
+ this.toasterService.error('Cannot update password for user : ' + error);
+ })
+ }
+}
diff --git a/src/app/components/user-management/user-detail/user-detail.component.html b/src/app/components/user-management/user-detail/user-detail.component.html
new file mode 100644
index 00000000..b4ba6cf0
--- /dev/null
+++ b/src/app/components/user-management/user-detail/user-detail.component.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
Creation date: {{user.created_at}}
+
Last update Date: {{user.updated_at}}
+
Last login: {{user.last_login}}
+
UUID: {{user.user_id}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/user-management/user-detail/user-detail.component.scss b/src/app/components/user-management/user-detail/user-detail.component.scss
new file mode 100644
index 00000000..a64b61ba
--- /dev/null
+++ b/src/app/components/user-management/user-detail/user-detail.component.scss
@@ -0,0 +1,7 @@
+.input-field {
+ width: 100%;
+}
+
+.button-div {
+ float: right;
+}
diff --git a/src/app/components/user-management/user-detail/user-detail.component.spec.ts b/src/app/components/user-management/user-detail/user-detail.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/user-management/user-detail/user-detail.component.ts b/src/app/components/user-management/user-detail/user-detail.component.ts
new file mode 100644
index 00000000..9edbae10
--- /dev/null
+++ b/src/app/components/user-management/user-detail/user-detail.component.ts
@@ -0,0 +1,110 @@
+import {Component, Inject, OnInit} from '@angular/core';
+import {FormControl, FormGroup, Validators} from "@angular/forms";
+import {Group} from "@models/groups/group";
+import {UserService} from "@services/user.service";
+import {ToasterService} from "@services/toaster.service";
+import {User} from "@models/users/user";
+import {Server} from "@models/server";
+import {userNameAsyncValidator} from "@components/user-management/add-user-dialog/userNameAsyncValidator";
+import {userEmailAsyncValidator} from "@components/user-management/add-user-dialog/userEmailAsyncValidator";
+import {ActivatedRoute, Router} from "@angular/router";
+import {Permission} from "@models/api/permission";
+import {Role} from "@models/api/role";
+import {AddUserDialogComponent} from "@components/user-management/add-user-dialog/add-user-dialog.component";
+import {MatDialog} from "@angular/material/dialog";
+import {ChangeUserPasswordComponent} from "@components/user-management/user-detail/change-user-password/change-user-password.component";
+import {RemoveToGroupDialogComponent} from "@components/group-details/remove-to-group-dialog/remove-to-group-dialog.component";
+
+@Component({
+ selector: 'app-user-detail',
+ templateUrl: './user-detail.component.html',
+ styleUrls: ['./user-detail.component.scss']
+})
+export class UserDetailComponent implements OnInit {
+
+ editUserForm: FormGroup;
+ groups: Group[];
+ user: User;
+ server: Server;
+ user_id: string;
+ permissions: Permission[];
+ changingPassword: boolean = false;
+
+ constructor(public userService: UserService,
+ private toasterService: ToasterService,
+ private route: ActivatedRoute,
+ private router: Router,
+ public dialog: MatDialog) {
+
+ }
+
+ ngOnInit(): void {
+ this.server = this.route.snapshot.data['server'];
+ if (!this.server) this.router.navigate(['/servers']);
+
+ this.route.data.subscribe((d: { server: Server; user: User, groups: Group[], permissions: Permission[]}) => {
+ this.user = d.user;
+ this.user_id = this.user.user_id;
+ this.groups = d.groups;
+ this.permissions = d.permissions;
+ this.initForm();
+ });
+
+ }
+
+ initForm() {
+ this.editUserForm = new FormGroup({
+ username: new FormControl(this.user.username, [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.pattern("[a-zA-Z0-9_-]+$")],
+ [userNameAsyncValidator(this.server, this.userService, this.user.username)]),
+ full_name: new FormControl(this.user.full_name),
+ email: new FormControl(this.user.email,
+ [Validators.email, Validators.required],
+ [userEmailAsyncValidator(this.server, this.userService, this.user.email)]),
+ is_active: new FormControl(this.user.is_active)
+ });
+ }
+
+ get form() {
+ return this.editUserForm.controls;
+ }
+
+ onEditClick() {
+ if (!this.editUserForm.valid) {
+ return;
+ }
+
+ const updatedUser = this.getUpdatedValues();
+ updatedUser['user_id'] = this.user.user_id;
+
+ this.userService.update(this.server, updatedUser)
+ .subscribe((user: User) => {
+ this.toasterService.success(`User ${user.username} updated`);
+ },
+ (error) => {
+ this.toasterService.error('Cannot update user : ' + error);
+ })
+ }
+
+ getUpdatedValues() {
+ let dirtyValues = {};
+
+ Object.keys(this.editUserForm.controls)
+ .forEach(key => {
+ const currentControl = this.editUserForm.get(key);
+
+ if (currentControl.dirty && currentControl.value !== this.user[key]) {
+ dirtyValues[key] = currentControl.value;
+ }
+ });
+
+ return dirtyValues;
+ }
+
+ onChangePassword() {
+ this.dialog.open(ChangeUserPasswordComponent,
+ {width: '400px', height: '300px', data: {user: this.user, server: this.server}});
+ }
+}
diff --git a/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.html b/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.html
new file mode 100644
index 00000000..9a47c641
--- /dev/null
+++ b/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.html
@@ -0,0 +1,16 @@
+
+
diff --git a/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.scss b/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.spec.ts b/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.spec.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.ts b/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.ts
new file mode 100644
index 00000000..fd96958f
--- /dev/null
+++ b/src/app/components/user-management/user-detail/user-permissions/user-permissions.component.ts
@@ -0,0 +1,64 @@
+import { Component, OnInit } from '@angular/core';
+import {Server} from "@models/server";
+import {Role} from "@models/api/role";
+import {Permission} from "@models/api/permission";
+import {ActivatedRoute, Router} from "@angular/router";
+import {MatDialog} from "@angular/material/dialog";
+import {ToasterService} from "@services/toaster.service";
+import {RoleService} from "@services/role.service";
+import {forkJoin} from "rxjs";
+import {Observable} from "rxjs/Rx";
+import {UserService} from "@services/user.service";
+import {User} from "@models/users/user";
+import {HttpErrorResponse} from "@angular/common/http";
+
+@Component({
+ selector: 'app-user-permissions',
+ templateUrl: './user-permissions.component.html',
+ styleUrls: ['./user-permissions.component.scss']
+})
+export class UserPermissionsComponent implements OnInit {
+
+ server: Server;
+ user: User;
+ userPermissions: Permission[];
+ permissions: Permission[];
+
+ constructor(private route: ActivatedRoute,
+ private dialog: MatDialog,
+ private toastService: ToasterService,
+ private router: Router,
+ private userService: UserService) {
+ this.route.data.subscribe((data: { server: Server, user: User, userPermissions: Permission[], permissions: Permission[] }) => {
+ this.server = data.server;
+ this.user = data.user;
+ this.userPermissions = data.userPermissions;
+ this.permissions = data.permissions;
+ });
+ }
+
+ ngOnInit(): void {
+ }
+
+ updatePermissions(toUpdate) {
+ const {add, remove} = toUpdate;
+ const obs: Observable[] = [];
+ add.forEach((permission: Permission) => {
+ obs.push(this.userService.addPermission(this.server, this.user.user_id, permission));
+ });
+ remove.forEach((permission: Permission) => {
+ obs.push(this.userService.removePermission(this.server, this.user.user_id, permission));
+ });
+
+ forkJoin(obs)
+ .subscribe(() => {
+ this.toastService.success(`permissions updated`);
+ this.router.navigate(['/server', this.server.id, 'management', 'users', this.user.user_id]);
+ },
+ (error: HttpErrorResponse) => {
+ this.toastService.error(`${error.message}
+ ${error.error.message}`);
+ });
+ }
+
+}
diff --git a/src/app/components/user-management/user-management.component.html b/src/app/components/user-management/user-management.component.html
index e453d528..a99f8fb6 100644
--- a/src/app/components/user-management/user-management.component.html
+++ b/src/app/components/user-management/user-management.component.html
@@ -1,3 +1,98 @@
-
- user-management works!
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Username
+
+ {{ row.username }}
+
+
+
+ Full Name
+
+ {{ row.full_name }}
+
+
+
+ Mail
+
+ {{ row.email }}
+
+
+
+ Active
+ {{row.is_active}}
+
+
+ Last Login
+ {{row.last_login}}
+
+
+ Last Update
+ {{row.updated_at ? row.updated_at : row.created_at}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/components/user-management/user-management.component.scss b/src/app/components/user-management/user-management.component.scss
index e69de29b..6b41b9a7 100644
--- a/src/app/components/user-management/user-management.component.scss
+++ b/src/app/components/user-management/user-management.component.scss
@@ -0,0 +1,42 @@
+.add-button {
+ height: 40px;
+ width: 160px;
+ margin: 20px;
+}
+
+.full-width {
+ width: 940px;
+ margin-left: -470px;
+ left: 50%;
+}
+
+.small-col {
+ flex-grow: 0.3;
+}
+
+.active-col {
+ flex-grow: 0.5;
+}
+
+.overflow-col {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ padding-right: 5px;
+}
+
+.custom-tooltip {
+ font-size:100px;
+ white-space: pre-line;
+}
+
+.loader {
+ position: absolute;
+ margin: auto;
+ height: 175px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ width: 175px;
+}
diff --git a/src/app/components/user-management/user-management.component.spec.ts b/src/app/components/user-management/user-management.component.spec.ts
index c5c67f32..e69de29b 100644
--- a/src/app/components/user-management/user-management.component.spec.ts
+++ b/src/app/components/user-management/user-management.component.spec.ts
@@ -1,28 +0,0 @@
-/* tslint:disable:no-unused-variable */
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { By } from '@angular/platform-browser';
-import { DebugElement } from '@angular/core';
-
-import { UserManagementComponent } from './user-management.component';
-
-describe('UserManagementComponent', () => {
- let component: UserManagementComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [UserManagementComponent]
- })
- .compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(UserManagementComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/components/user-management/user-management.component.ts b/src/app/components/user-management/user-management.component.ts
index a374bb28..b5d39e93 100644
--- a/src/app/components/user-management/user-management.component.ts
+++ b/src/app/components/user-management/user-management.component.ts
@@ -1,4 +1,30 @@
-import { Component, OnInit } from '@angular/core';
+/*
+* 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, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
+import {ActivatedRoute, Router} from "@angular/router";
+import {Server} from "@models/server";
+import {MatSort} from "@angular/material/sort";
+import {UserService} from "@services/user.service";
+import {ProgressService} from "../../common/progress/progress.service";
+import {User} from "@models/users/user";
+import {SelectionModel} from "@angular/cdk/collections";
+import {AddUserDialogComponent} from "@components/user-management/add-user-dialog/add-user-dialog.component";
+import {MatDialog} from "@angular/material/dialog";
+import {DeleteUserDialogComponent} from "@components/user-management/delete-user-dialog/delete-user-dialog.component";
+import {ToasterService} from "@services/toaster.service";
+import {MatPaginator} from "@angular/material/paginator";
+import {MatTableDataSource} from "@angular/material/table";
+import {ServerService} from "@services/server.service";
@Component({
selector: 'app-user-management',
@@ -6,10 +32,124 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./user-management.component.scss']
})
export class UserManagementComponent implements OnInit {
+ server: Server;
+ dataSource = new MatTableDataSource();
+ displayedColumns = ['select', 'username', 'full_name', 'email', 'is_active', 'last_login', 'updated_at', 'delete'];
+ selection = new SelectionModel(true, []);
+ searchText = '';
- constructor() { }
+ @ViewChildren('usersPaginator') usersPaginator: QueryList;
+ @ViewChildren('usersSort') usersSort: QueryList;
+ isReady = false;
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private userService: UserService,
+ private progressService: ProgressService,
+ private serverService: ServerService,
+ public dialog: MatDialog,
+ private toasterService: ToasterService) { }
ngOnInit() {
+ const serverId = this.route.parent.snapshot.paramMap.get('server_id');
+ this.serverService.get(+serverId).then((server: Server) => {
+ this.server = server;
+ this.refresh();
+ });
}
+ ngAfterViewInit() {
+ this.usersPaginator.changes.subscribe((comps: QueryList ) =>
+ {
+ this.dataSource.paginator = comps.first;
+ });
+ this.usersSort.changes.subscribe((comps: QueryList) => {
+ this.dataSource.sort = comps.first;
+ })
+
+ this.dataSource.sortingDataAccessor = (item, property) => {
+ switch (property) {
+ case 'username':
+ case 'full_name':
+ case 'email':
+ return item[property] ? item[property].toLowerCase() : '';
+ default:
+ return item[property];
+ }
+ };
+ }
+
+ refresh() {
+ this.userService.list(this.server).subscribe(
+ (users: User[]) => {
+ this.isReady = true;
+ this.dataSource.data = users;
+ },
+ (error) => {
+ this.progressService.setError(error);
+ }
+ );
+ }
+
+ addUser() {
+ const dialogRef = this.dialog.open(AddUserDialogComponent, {
+ width: '400px',
+ autoFocus: false,
+ disableClose: true,
+ });
+ let instance = dialogRef.componentInstance;
+ instance.server = this.server;
+ dialogRef.afterClosed().subscribe(() => this.refresh());
+ }
+
+ onDelete(user: User) {
+ this.dialog
+ .open(DeleteUserDialogComponent, {width: '500px', data: {users: [user]}})
+ .afterClosed()
+ .subscribe((isDeletedConfirm) => {
+ if (isDeletedConfirm) {
+ this.userService.delete(this.server, user.user_id)
+ .subscribe(() => {
+ this.refresh()
+ }, (error) => {
+ this.toasterService.error(`An error occur while trying to delete user ${user.username}`);
+ });
+ }
+ });
+ }
+
+
+
+ isAllSelected() {
+ const numSelected = this.selection.selected.length;
+ const numRows = this.dataSource.data.length;
+ return numSelected === numRows;
+ }
+
+ masterToggle() {
+ this.isAllSelected() ?
+ this.selection.clear() :
+ this.dataSource.data.forEach(row => this.selection.select(row));
+ }
+
+ deleteMultiple() {
+ this.dialog
+ .open(DeleteUserDialogComponent, {width: '500px', data: {users: this.selection.selected}})
+ .afterClosed()
+ .subscribe((isDeletedConfirm) => {
+ if (isDeletedConfirm) {
+ this.selection.selected.forEach((user: User) => {
+ this.userService.delete(this.server, user.user_id)
+ .subscribe(() => {
+ this.refresh()
+ }, (error) => {
+ this.toasterService.error(`An error occur while trying to delete user ${user.username}`);
+ });
+ })
+ this.selection.clear();
+ }
+ });
+
+ }
}
diff --git a/src/app/filters/group-filter.pipe.ts b/src/app/filters/group-filter.pipe.ts
new file mode 100644
index 00000000..8bda5a94
--- /dev/null
+++ b/src/app/filters/group-filter.pipe.ts
@@ -0,0 +1,33 @@
+/*
+* 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 {Group} from "../models/groups/group";
+import {MatTableDataSource} from "@angular/material/table";
+
+@Pipe({
+ name: 'groupFilter'
+})
+export class GroupFilterPipe implements PipeTransform {
+
+ transform(groups: MatTableDataSource, searchText: string): MatTableDataSource {
+
+ if (!searchText) {
+ return groups;
+ }
+
+ searchText = searchText.trim().toLowerCase();
+ groups.filter = searchText;
+ return groups;
+ }
+
+}
diff --git a/src/app/filters/user-filter.pipe.ts b/src/app/filters/user-filter.pipe.ts
new file mode 100644
index 00000000..fec797e9
--- /dev/null
+++ b/src/app/filters/user-filter.pipe.ts
@@ -0,0 +1,33 @@
+/*
+* 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 {User} from "@models/users/user";
+import {MatTableDataSource} from "@angular/material/table";
+
+@Pipe({
+ name: 'userFilter'
+})
+export class UserFilterPipe implements PipeTransform {
+
+ transform(items: MatTableDataSource, searchText: string) {
+ if (!items) return [];
+ if (!searchText) return items;
+ searchText = searchText.toLowerCase();
+ return items.data.filter((item: User) => {
+ return (item.username && item.username.toLowerCase().includes(searchText))
+ || (item.full_name && item.full_name.toLowerCase().includes(searchText))
+ || (item.email && item.email.toLowerCase().includes(searchText));
+ });
+ }
+
+}
diff --git a/src/app/layouts/default-layout/default-layout.component.html b/src/app/layouts/default-layout/default-layout.component.html
index 0eb6c2e1..53957dea 100644
--- a/src/app/layouts/default-layout/default-layout.component.html
+++ b/src/app/layouts/default-layout/default-layout.component.html
@@ -23,23 +23,28 @@
settings
Settings
-