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

Groups {{group.name}} details

+
+ + +
+
+ + Group name: + + +
+ +
+ Is build in +
+
+ +
+
+
+
Creation date: {{group.created_at}}
+
Last update Date: {{group.updated_at}}
+
UUID: {{group.user_group_id}}
+
+
+ +
+
+ person_add +
+ + + +
+
+ +
+
+
{{role.name}}
+
+ +
+
+
+
+
+
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 @@ +
+
Confirm ?
+
Removing: {{data.name}}
+
+
+ + +
+ 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

+
+ + + Group name is required + Group name is incorrect + Group with this name exists + +
+ +
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 @@ +
+
+
+

Groups management

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

+
+ + + + role name is required + + + Role name is incorrect + + + + + +
+ + +
+
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 @@ +
+
+ + +
+
+

+
Permission to Add:
+
+ +
+
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 @@ +
+
+
+ Allow: +
+
+
+ Deny: +
+
+
+ + + + {{option.name}} + + +
+ + +
+
+ + +
+
+
Owned
+ + + + +
+ +
+
Available
+ + + + +
+
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 @@ +
+
+
+ + keyboard_arrow_left + +

Role {{role.name}} details

+
+
+
+
+ + Role name: + + +
+
+ + Description: + + +
+
Creation date: {{role.created_at}}
+
Last update Date: {{role.updated_at}}
+
UUID: {{role.role_id}}
+
+ Is build in +
+
+ +
+
+ +
+
+
Permissions
+
+ +
+
+ + +
+
+
+
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 @@ +
+ +
+ Edit {{role.name}} role permissions +
+
+ 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 @@ +
+
+
+

Roles Management

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

+
+ + + Username is required + Username is incorrect + User with this username exists + + + + + + + Email is required + Email is invalid + + + + + + A password between 6 and 100 characters is required. + + + + Password and Confirm password must be the same. + + + + Is active + +
Add user to groups :
+ + + Groups + + + + {{group.name}} + + + + +
+
+
+
{{group.name}}
+ delete +
+
+
+ + +
+ + +
+
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 :

+
+
+ + + Password must be between 6 and 100 characters. + + + + + Password and Confirm password must be the same. + + + +
+ + +
+
+
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 @@ +
+
+
+ +

User Details

+
+
+
+ + +
+
+ + + Username is required + + Username is incorrect + + User with this username exists + + + + + + + + Email is required + + Email is invalid + + User with this email exists + + + + Is active +
+ Is Superadmin +
+ +
+ +
+
+ + + +
+
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 @@ +
+ +
+ Edit {{user.username}} role permissions +
+
+ 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! -

+
+
+
+

User Management

+ + +
+
+ +
+ + + +
+ +
+
+ + + + + + + + + + + + + + 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 - - - - diff --git a/src/app/layouts/default-layout/default-layout.component.ts b/src/app/layouts/default-layout/default-layout.component.ts index c2e8cfe2..9ab73899 100644 --- a/src/app/layouts/default-layout/default-layout.component.ts +++ b/src/app/layouts/default-layout/default-layout.component.ts @@ -9,7 +9,7 @@ import { RecentlyOpenedProjectService } from '../../services/recentlyOpenedProje import { ControllerManagementService } from '../../services/controller-management.service'; import { ToasterService } from '../../services/toaster.service'; import { version } from './../../version'; -import{ Controller } from '../../models/controller'; +import { Controller } from '../../models/controller'; @Component({ selector: 'app-default-layout', @@ -28,6 +28,7 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy { recentlyOpenedcontrollerId: string; recentlyOpenedProjectId: string; + controllerIdProjectList: string; constructor( diff --git a/src/app/models/LocalStorage.ts b/src/app/models/LocalStorage.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/models/api/permission.ts b/src/app/models/api/permission.ts new file mode 100644 index 00000000..3a187279 --- /dev/null +++ b/src/app/models/api/permission.ts @@ -0,0 +1,35 @@ +/* +* 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 enum Methods { + GET = 'GET', + HEAD = 'HEAD', + POST = 'POST', + PATCH = 'PATCH', + PUT = 'PUT', + DELETE = 'DELETE' +} + +export enum PermissionActions { + ALLOW = 'ALLOW', + DENY = 'DENY' +} + +export interface Permission { + methods: Methods[]; + path: string; + action: PermissionActions; + description: string; + created_at?: string; + updated_at?: string; + permission_id?: string; +} diff --git a/src/app/models/api/role.ts b/src/app/models/api/role.ts new file mode 100644 index 00000000..16519371 --- /dev/null +++ b/src/app/models/api/role.ts @@ -0,0 +1,11 @@ +import {Permission} from "./permission"; + +export interface Role { + name: string; + description: string; + created_at: string; + updated_at: string; + role_id: string; + is_builtin: boolean; + permissions: Permission[]; +} diff --git a/src/app/models/groups/group.ts b/src/app/models/groups/group.ts new file mode 100644 index 00000000..3996f52a --- /dev/null +++ b/src/app/models/groups/group.ts @@ -0,0 +1,19 @@ +/* +* 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 interface Group { + name: string; + created_at: string; + updated_at: string; + user_group_id: string; + is_builtin: boolean; +} diff --git a/src/app/models/users/user.ts b/src/app/models/users/user.ts index 880734dd..a385cc85 100644 --- a/src/app/models/users/user.ts +++ b/src/app/models/users/user.ts @@ -2,6 +2,7 @@ export interface User { created_at: string; email: string; full_name: string; + last_login: string; is_active: boolean; is_superadmin: boolean; updated_at: string; diff --git a/src/app/resolvers/group-members.resolver.ts b/src/app/resolvers/group-members.resolver.ts new file mode 100644 index 00000000..f9a1e483 --- /dev/null +++ b/src/app/resolvers/group-members.resolver.ts @@ -0,0 +1,50 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import { + Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, Subscriber} from 'rxjs'; +import {ServerService} from "../services/server.service"; +import {GroupService} from "../services/group.service"; +import {Server} from "../models/server"; +import {Group} from "../models/groups/group"; +import {User} from "../models/users/user"; + +@Injectable({ + providedIn: 'root' +}) +export class GroupMembersResolver implements Resolve { + + constructor(private serverService: ServerService, + private groupService: GroupService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + + return new Observable((subscriber: Subscriber) => { + + const serverId = route.paramMap.get('server_id'); + const groupId = route.paramMap.get('user_group_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.groupService.getGroupMember(server, groupId).subscribe((users: User[]) => { + subscriber.next(users); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/resolvers/group-role.resolver.ts b/src/app/resolvers/group-role.resolver.ts new file mode 100644 index 00000000..a16b0e7a --- /dev/null +++ b/src/app/resolvers/group-role.resolver.ts @@ -0,0 +1,50 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {ServerService} from "../services/server.service"; +import {GroupService} from "../services/group.service"; +import {User} from "../models/users/user"; +import {Server} from "../models/server"; +import {Role} from "../models/api/role"; + +@Injectable({ + providedIn: 'root' +}) +export class GroupRoleResolver implements Resolve { + + constructor(private serverService: ServerService, + private groupService: GroupService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + + return new Observable((subscriber: Subscriber) => { + + const serverId = route.paramMap.get('server_id'); + const groupId = route.paramMap.get('user_group_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.groupService.getGroupRole(server, groupId).subscribe((role: Role[]) => { + subscriber.next(role); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/resolvers/group.resolver.ts b/src/app/resolvers/group.resolver.ts new file mode 100644 index 00000000..147d127f --- /dev/null +++ b/src/app/resolvers/group.resolver.ts @@ -0,0 +1,51 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {ServerService} from "@services/server.service"; +import {GroupService} from "@services/group.service"; +import {User} from "@models/users/user"; +import {Server} from "@models/server"; +import {Group} from "@models/groups/group"; + +@Injectable({ + providedIn: 'root' +}) +export class GroupResolver implements Resolve { + + + constructor(private serverService: ServerService, + private groupService: GroupService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + + return new Observable((subscriber: Subscriber) => { + + const serverId = route.paramMap.get('server_id'); + const groupId = route.paramMap.get('user_group_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.groupService.get(server, groupId).subscribe((group: Group) => { + subscriber.next(group); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/resolvers/permission.resolver.ts b/src/app/resolvers/permission.resolver.ts new file mode 100644 index 00000000..176e8572 --- /dev/null +++ b/src/app/resolvers/permission.resolver.ts @@ -0,0 +1,47 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { Injectable } from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {Permission} from "@models/api/permission"; +import {PermissionsService} from "@services/permissions.service"; +import {ServerService} from "@services/server.service"; +import {Server} from "@models/server"; + +@Injectable({ + providedIn: 'root' +}) +export class PermissionResolver implements Resolve { + + constructor(private permissionService: PermissionsService, + private serverService: ServerService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return new Observable((observer: Subscriber) => { + const serverId = route.paramMap.get('server_id'); + this.serverService.get(+serverId).then((server: Server) => { + this.permissionService.list(server).subscribe((permission: Permission[]) => { + observer.next(permission); + observer.complete(); + }); + }); + }); + + + } +} diff --git a/src/app/resolvers/role-detail.resolver.ts b/src/app/resolvers/role-detail.resolver.ts new file mode 100644 index 00000000..2bec400b --- /dev/null +++ b/src/app/resolvers/role-detail.resolver.ts @@ -0,0 +1,48 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {Server} from "../models/server"; +import {Role} from "../models/api/role"; +import {ServerService} from "../services/server.service"; +import {RoleService} from "../services/role.service"; + +@Injectable({ + providedIn: 'root' +}) +export class RoleDetailResolver implements Resolve { + + constructor(private serverService: ServerService, + private roleService: RoleService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return new Observable((observer: Subscriber) => { + const serverId = route.paramMap.get('server_id'); + const roleId = route.paramMap.get('role_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.roleService.getById(server, roleId).subscribe((role: Role) => { + observer.next( role); + observer.complete(); + }); + }); + }); + + } +} diff --git a/src/app/resolvers/user-detail.resolver.ts b/src/app/resolvers/user-detail.resolver.ts new file mode 100644 index 00000000..5ea8b6dc --- /dev/null +++ b/src/app/resolvers/user-detail.resolver.ts @@ -0,0 +1,48 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { Injectable } from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {ServerService} from "@services/server.service"; +import {UserService} from "@services/user.service"; +import {User} from "@models/users/user"; +import {Server} from "@models/server"; + +@Injectable({ + providedIn: 'root' +}) +export class UserDetailResolver implements Resolve { + + constructor(private serverService: ServerService, + private userService: UserService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return new Observable((subscriber: Subscriber) => { + + const serverId = route.paramMap.get('server_id'); + const userId = route.paramMap.get('user_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.userService.get(server, userId).subscribe((user: User) => { + subscriber.next(user); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/resolvers/user-groups.resolver.ts b/src/app/resolvers/user-groups.resolver.ts new file mode 100644 index 00000000..9a03ce64 --- /dev/null +++ b/src/app/resolvers/user-groups.resolver.ts @@ -0,0 +1,48 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { Injectable } from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {Group} from "../models/groups/group"; +import {User} from "../models/users/user"; +import {Server} from "../models/server"; +import {ServerService} from "../services/server.service"; +import {UserService} from "../services/user.service"; + +@Injectable({ + providedIn: 'root' +}) +export class UserGroupsResolver implements Resolve { + constructor(private serverService: ServerService, + private userService: UserService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return new Observable((subscriber: Subscriber) => { + + const serverId = route.paramMap.get('server_id'); + const userId = route.paramMap.get('user_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.userService.getGroupsByUserId(server, userId).subscribe((groups: Group[]) => { + subscriber.next(groups); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/resolvers/user-permissions.resolver.ts b/src/app/resolvers/user-permissions.resolver.ts new file mode 100644 index 00000000..5fd0000f --- /dev/null +++ b/src/app/resolvers/user-permissions.resolver.ts @@ -0,0 +1,48 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { Injectable } from '@angular/core'; +import { + Router, Resolve, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import {Observable, of, Subscriber} from 'rxjs'; +import {ServerService} from "../services/server.service"; +import {UserService} from "../services/user.service"; +import {Server} from "../models/server"; +import {Permission} from "../models/api/permission"; + +@Injectable({ + providedIn: 'root' +}) +export class UserPermissionsResolver implements Resolve { + + constructor(private serverService: ServerService, + private userService: UserService) { + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return new Observable((subscriber: Subscriber) => { + + const serverId = route.paramMap.get('server_id'); + const userId = route.paramMap.get('user_id'); + + this.serverService.get(+serverId).then((server: Server) => { + this.userService.getPermissionsByUserId(server, userId).subscribe((permissions: Permission[]) => { + subscriber.next(permissions); + subscriber.complete(); + }); + }); + }); + } +} diff --git a/src/app/services/ApiInformation/ApiInformationCache.ts b/src/app/services/ApiInformation/ApiInformationCache.ts new file mode 100644 index 00000000..448c4d0f --- /dev/null +++ b/src/app/services/ApiInformation/ApiInformationCache.ts @@ -0,0 +1,47 @@ +import {IApiData} from "./IApiData"; +import {Server} from "../../models/server"; +import {IExtraParams} from "./IExtraParams"; +import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject"; + +/** + * create cache to keep server information on client side + * reduce number of requests to the server + */ +export class ApiInformationCache { + + private cache = new Map(); + + + public update(server: Server, key: string, value: string, extraParams: IExtraParams[], data: IGenericApiObject[]) { + const dataKey = this.generateKey(server, key, value, extraParams); + this.cache.set(dataKey, {expired: Date.now() + 10000, data}); + } + + public get(server: Server, key: string, value: string, extraParams: IExtraParams[]): IGenericApiObject[] | undefined { + const dataKey = this.generateKey(server, key, value, extraParams); + const data = this.cache.get(dataKey); + if (data) { + if (data.expired > Date.now()) { + return data.data; + } + } + + } + + private generateKey(server: Server, key: string, value: string, extraParams: IExtraParams[]) { + return `${server.id}-${key}-${value}-${extraParams.map(param => `${param.key}+${param.value}`).join('.')}`; + } + + searchByName(name: string) { + const result: IGenericApiObject[] = []; + this.cache.forEach((apiData: IApiData) => { + apiData.data.forEach((value: IGenericApiObject) => { + if (value.name.includes(name)) { + result.push(value); + } + }); + }); + + return result; + } +} diff --git a/src/app/services/ApiInformation/GetObjectIdHelper.ts b/src/app/services/ApiInformation/GetObjectIdHelper.ts new file mode 100644 index 00000000..e189a5a7 --- /dev/null +++ b/src/app/services/ApiInformation/GetObjectIdHelper.ts @@ -0,0 +1,123 @@ +import {ApiInformationService, IApiObject} from "@services/ApiInformation/api-information.service"; +import {Server} from "@models/server"; +import {IExtraParams} from "@services/ApiInformation/IExtraParams"; +import {forkJoin, Observable, of} from "rxjs"; +import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject"; +import {map} from "rxjs/operators"; + +export class GetObjectIdHelper { + + public static getIdNameFromKey(key: string): string { + return /{([^)]+)}/.exec(key)[1]; + } + + /** + * find the GET query corresponding to the key in Object list + * @param key + */ + public static findElementInObjectListFn(key): (data: IApiObject[]) => IApiObject { + return function findElement(data: IApiObject[]): IApiObject { + const elem = data.find(d => d.name === key); + if (!elem) { + throw new Error('entry not found'); + } + return elem; + }; + } + + /** + * Build the request, append the value if required + * @param server + * @param value + * @param extraParams + */ + public static buildRequestURL(server: Server, value: string, extraParams: IExtraParams[]): (elem) => string { + return (elem): string => { + let url = `${server.protocol}//${server.host}:${server.port}${elem.path}`; + if (extraParams) { + extraParams.forEach((param) => { + url = url.replace(param.key, param.value); + }); + } + + if (value) { + url = `${url}/${value}`; + } + return url; + }; + } + + /** + * Map the data from server to a generic response object + * @param key + * @param extraParams + * @param service + * @param server + */ + public static createResponseObject(key: string, + extraParams: IExtraParams[], + service: ApiInformationService, + server: Server + ): (response) => Observable { + + const idName = key ? GetObjectIdHelper.getIdNameFromKey(key) : undefined; + return (response): Observable => { + + if (!(response instanceof Array)) { + response = [response]; + } + if (response.length === 0) { + return of([]); + } + + /* + specific treatment for link_id + */ + if (key === '{link_id}') { + return GetObjectIdHelper.setLinkObjectInformation(response, extraParams, service, server); + } else { + return GetObjectIdHelper.setGenericObjectInformation(response, idName); + } + }; + } + + private static setGenericObjectInformation(response: any[], idName: string): Observable { + const keys = Object.keys(response[0]); + const idKey = keys.find(k => k.match(/_id$|filename/)); + const nameKey = keys.find(k => k.match(/name/)); + response = response.map(o => { + return { + id: o[idName] || o[idKey], + name: o[nameKey] + }; + }); + return of(response); + } + + private static setLinkObjectInformation(links: any[], + extraParams: IExtraParams[], + service: ApiInformationService, + server: Server + ): Observable { + + return forkJoin(links.map(link => GetObjectIdHelper.getLinkInformation(link, extraParams, service, server))); + } + + private static getLinkInformation(link: any, + extraParams: IExtraParams[], + service: ApiInformationService, + server: Server + ): Observable { + + const nodesDataObs = link.nodes.map(node => service.getListByObjectId(server, '{node_id}', node.node_id, extraParams)); + return forkJoin(nodesDataObs) + .pipe(map((nodes: [any]) => { + const name = nodes + .reduce((acc, val) => acc.concat(val), []) + .map(node => node.name) + .join(' <-> '); + + return {id: link.link_id, name}; + })); + } +} diff --git a/src/app/services/ApiInformation/IApiData.ts b/src/app/services/ApiInformation/IApiData.ts new file mode 100644 index 00000000..b4b2505e --- /dev/null +++ b/src/app/services/ApiInformation/IApiData.ts @@ -0,0 +1,6 @@ +import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject"; + +export interface IApiData { + expired: number; + data: IGenericApiObject[]; +} diff --git a/src/app/services/ApiInformation/IExtraParams.ts b/src/app/services/ApiInformation/IExtraParams.ts new file mode 100644 index 00000000..146914cb --- /dev/null +++ b/src/app/services/ApiInformation/IExtraParams.ts @@ -0,0 +1,8 @@ +/** + * key value association to query the api documentation + * ex : {key: 'project_id', value: 'd6381517-4ac6-436e-bff7-b667dd64f693'} + */ +export interface IExtraParams { + key: string; + value: string; +} diff --git a/src/app/services/ApiInformation/IGenericApiObject.ts b/src/app/services/ApiInformation/IGenericApiObject.ts new file mode 100644 index 00000000..e2247e3a --- /dev/null +++ b/src/app/services/ApiInformation/IGenericApiObject.ts @@ -0,0 +1,7 @@ +/** + * {id: 'project_id, name: 'test project'} + */ +export interface IGenericApiObject { + id: string; + name?: string; +} diff --git a/src/app/services/ApiInformation/api-information.service.spec.ts b/src/app/services/ApiInformation/api-information.service.spec.ts new file mode 100644 index 00000000..c3c4ab5b --- /dev/null +++ b/src/app/services/ApiInformation/api-information.service.spec.ts @@ -0,0 +1,310 @@ +import {ApiInformationService, IPathDict} from "@services/ApiInformation/api-information.service"; +import {HttpClient} from "@angular/common/http"; +import {fakeAsync, TestBed, tick} from "@angular/core/testing"; +import {DisplayPathPipe} from "@components/permissions-management/display-path.pipe"; +import {Observable, of, ReplaySubject} from "rxjs"; +import {Server} from "@models/server"; +import {getTestServer} from "@services/testing"; +import {Methods} from "@models/api/permission"; +import {ApiInformationCache} from "@services/ApiInformation/ApiInformationCache"; +import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject"; + +describe('ApiInformationService', () => { + let apiService: ApiInformationService; + let httpClientSpy: jasmine.SpyObj; + let server: Server; + + beforeEach(() => { + const spy = jasmine.createSpyObj('HttpClient', ['get']); + TestBed.configureTestingModule({ + providers: [ApiInformationService, {provide: HttpClient, useValue: spy}], + }); + httpClientSpy = TestBed.inject(HttpClient) as jasmine.SpyObj + httpClientSpy.get.and.returnValue(new Observable()); + apiService = TestBed.inject(ApiInformationService); + server = getTestServer(); + }); + + describe('ApiInformationService.getMethods() tests', () => { + it('create an instance', () => { + expect(apiService).toBeTruthy(); + }); + + it('Should return methods for /projects/{project_id}', fakeAsync(() => { + let res: Methods[]; + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }, { + methods: ['GET', 'POST'], + originalPath: '/v3/projects/{project_id}/nodes', + path: '/projects/{project_id}/nodes', + subPaths: ['projects', '{project_id}', 'nodes'], + }]; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getMethods('/projects/{project_id}').subscribe(data => { + res = data; + }); + tick(); + expect(res).toContain(Methods.GET) + expect(res).toContain(Methods.PUT) + expect(res).toContain(Methods.POST) + })); + + it('Should return empty array if no data available', fakeAsync(() => { + let res: Methods[]; + const mockGetPath: IPathDict[] = []; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getMethods('/projects/{project_id}').subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(0); + })); + + + }) + + describe('ApiInformationService.getPath() tests', () => { + + it('Should return array of 2', fakeAsync(() => { + + let res: IPathDict[]; + const mockData: ReplaySubject = new ReplaySubject(1); + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }, { + methods: ['GET', 'POST'], + originalPath: '/v3/projects/{project_id}/nodes', + path: '/projects/{project_id}/nodes', + subPaths: ['projects', '{project_id}', 'nodes'], + }]; + apiService['data'] = mockData; + apiService['data'].next(mockGetPath); + apiService.getPath('/projects/{project_id}').subscribe((data) => { + res = data; + }); + tick(); + expect(res.length).toBe(2); + expect(res).toContain(mockGetPath[0]); + expect(res).toContain(mockGetPath[1]); + })); + + it('Should return empty array if ApiInformationService.data does not have info about nodes', fakeAsync(() => { + + let res: IPathDict[]; + const mockIPathDict: IPathDict[] = [{ + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }, { + methods: ['GET', 'POST'], + originalPath: '/v3/projects/{project_id}/nodes', + path: '/projects/{project_id}/nodes', + subPaths: ['projects', '{project_id}', 'nodes'], + }]; + const mockData: ReplaySubject = new ReplaySubject(1); + apiService['data'] = mockData; + apiService['data'].next(mockIPathDict); + + apiService.getPath('/nodes').subscribe( (data) => { + res = data; + }); + tick(); + expect(res.length).toBe(0); + })); + + it('Should return array of 1 ', fakeAsync(() => { + + let res: IPathDict[]; + const mockData: ReplaySubject = new ReplaySubject(1); + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }, { + methods: ['GET', 'POST'], + originalPath: '/v3/projects/{project_id}/nodes', + path: '/projects/{project_id}/nodes', + subPaths: ['projects', '{project_id}', 'nodes'], + }, { + methods: ['GET', 'PUT', 'DELETE'], + originalPath: '/v3/projects/{project_id}/nodes/{node_id}', + path: '/projects/{project_id}/nodes/{node_id}', + subPaths: ['projects', '{project_id}', 'nodes', '{node_id}'], + }]; + apiService['data'] = mockData; + apiService['data'].next(mockGetPath); + apiService.getPath('/projects/tralala/nodes/bidule').subscribe((data) => { + res = data; + }); + tick(); + expect(res.length).toBe(1) + expect(res).toContain(mockGetPath[2]) + })); + + + }); + + describe('ApiInformationService.getPathNextElement tests ', () => { + it('Should return next path elements possible', fakeAsync(() => { + let res: string[]; + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }, { + methods: ['GET', 'POST'], + originalPath: '/v3/projects/{project_id}/nodes', + path: '/projects/{project_id}/nodes', + subPaths: ['projects', '{project_id}', 'nodes'], + }]; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getPathNextElement(['projects','{project_id}']).subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(1); + expect(res).toContain('nodes'); + })); + + it('Should return no next path elements for /projects/{project_id}/nodes', fakeAsync(() => { + let res: string[]; + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }, { + methods: ['GET', 'POST'], + originalPath: '/v3/projects/{project_id}/nodes', + path: '/projects/{project_id}/nodes', + subPaths: ['projects', '{project_id}', 'nodes'], + }]; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getPathNextElement(['projects', '{project_id}', 'nodes']).subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(0); + })); + + it('Should return no next path elements for /templates', fakeAsync(() => { + let res: string[]; + const mockGetPath: IPathDict[] = []; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getPathNextElement(['templates']).subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(0); + })); + + }); + + describe('ApiInformationService.getKeysForPath tests ', () => { + it('Should return key/value pairs for path /projects/tralala/nodes/bidule', fakeAsync(() => { + let res: { key: string; value: string }[]; + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'PUT', 'DELETE'], + originalPath: '/v3/projects/{project_id}/nodes/{node_id}', + path: '/projects/{project_id}/nodes/{node_id}', + subPaths: ['projects', '{project_id}', 'nodes', '{node_id}'], + }, { + methods: ['GET'], + originalPath: '/v3/projects/{project_id}/nodes/{node_id}/links', + path: '/projects/{project_id}/nodes/{node_id}/links', + subPaths: ['projects', '{project_id}', 'nodes', '{node_id}', 'links'], + }]; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getKeysForPath('/projects/tralala/nodes/bidule').subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(2); + expect(res).toContain({key: '{project_id}', value: 'tralala'}); + expect(res).toContain({key: '{node_id}', value: 'bidule'}); + })); + + it('Should return no key/value pairs for path /projects', fakeAsync(() => { + let res: { key: string; value: string }[]; + const mockGetPath: IPathDict[] = [{ + methods: ['GET', 'POST'], + originalPath: '/v3/projects', + path: '/projects', + subPaths: ['projects'], + }, { + methods: ['GET', 'DELETE', 'PUT'], + originalPath: '/v3/projects/{project_id}', + path: '/projects/{project_id}', + subPaths: ['projects', '{project_id}'], + }]; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getKeysForPath('/projects').subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(0); + })); + + xit('Should return no key/value pairs for path /projects/tralala if no data available', fakeAsync(() => { + let res: { key: string; value: string }[]; + const mockGetPath: IPathDict[] = []; + spyOn(apiService, 'getPath').and.returnValue(of(mockGetPath)); + apiService.getKeysForPath('/projects').subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(0); + })); + + }); + + describe('ApiInformationService.getListByObjectId tests', () => { + + it('Should', fakeAsync(() => { + let res: IGenericApiObject[]; + const mockGetCache: IGenericApiObject[] = [{id: 'id-tralala', name: 'tralala-project'}]; + spyOn(apiService['cache'], 'get').and.returnValue(mockGetCache); + apiService.getListByObjectId(server, '{project_id}', 'id-tralala').subscribe(data => { + res = data; + }); + tick(); + expect(res.length).toBe(1); + expect(res).toContain(mockGetCache[0]) + })); + + + + + }); + + describe('ApiInformationService.getIdByObjNameFromCache tests ', () => { + it('Should get possible id of object whose name contains tralala', () => { + const mockGetCache: IGenericApiObject[] = [{id: 'id-tralala', name: 'tralala-project'}, + {id: 'id-tralala2', name: 'other-tralala-project'}]; + spyOn(apiService['cache'], 'searchByName').and.returnValue(mockGetCache); + const res = apiService.getIdByObjNameFromCache('tralala'); + expect(res.length).toBe(2); + expect(res).toContain(mockGetCache[0]); + expect(apiService['cache'].searchByName).toHaveBeenCalled(); + }) + + it('Should get empty array for object whose name contains tralala', () => { + const mockGetCache: IGenericApiObject[] = []; + spyOn(apiService['cache'], 'searchByName').and.returnValue(mockGetCache); + const res = apiService.getIdByObjNameFromCache('tralala'); + expect(res.length).toBe(0); + expect(apiService['cache'].searchByName).toHaveBeenCalled(); + }) + }); + +}); diff --git a/src/app/services/ApiInformation/api-information.service.ts b/src/app/services/ApiInformation/api-information.service.ts new file mode 100644 index 00000000..cec2adb0 --- /dev/null +++ b/src/app/services/ApiInformation/api-information.service.ts @@ -0,0 +1,278 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {Observable, of, ReplaySubject} from "rxjs"; +import {map, switchMap, take, tap} from "rxjs/operators"; +import {Methods} from "app/models/api/permission"; +import {HttpServer} from "app/services/http-server.service"; +import {Server} from "app/models/server"; +import {GetObjectIdHelper} from "@services/ApiInformation/GetObjectIdHelper"; +import {IExtraParams} from "@services/ApiInformation/IExtraParams"; +import {ApiInformationCache} from "@services/ApiInformation/ApiInformationCache"; +import {IGenericApiObject} from "@services/ApiInformation/IGenericApiObject"; + +/** + * representation of an APi endpoint + * { + * methods: ['GET', 'DELETE', 'PUT'], + * originalPath: '/v3/projects/{project_id}', + * path: '/projects/{project_id}', + * subPaths: ['projects', '{project_id}'], + } + */ +export interface IPathDict { + methods: ('POST' | 'GET' | 'PUT' | 'DELETE' | 'HEAD' | 'PATH')[]; + originalPath: string; + path: string; + subPaths: string[]; +} + +/** + * name: 'node_id', + * path: '/projects/{project_id}/nodes/{node_id} + */ +export interface IApiObject { + name: string; + path: string; +} + +export interface IQueryObject { + id: string; + text: string[]; +} + +@Injectable({ + providedIn: 'root' +}) +export class ApiInformationService { + + private cache = new ApiInformationCache(); + private allowed = ['projects', 'images', 'templates', 'computes', 'symbols', 'notifications']; + private data: ReplaySubject = new ReplaySubject(1); + private objs: ReplaySubject = new ReplaySubject(1); + public readonly bracketIdRegex = new RegExp("\{(.*?)\}", 'g'); + public readonly uuidRegex = new RegExp("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"); + public readonly finalBracketIdRegex = new RegExp("\{(.*?)\}$"); + + + constructor(private httpClient: HttpClient) { + this.loadLocalInformation(); + + this.data.subscribe((data) => { + localStorage.setItem('api-definition', JSON.stringify(data)); + }); + + this.objs.subscribe((data) => { + localStorage.setItem('api-definition-objs', JSON.stringify(data)); + }); + + + this.httpClient + .get(`https://apiv3.gns3.net/openapi.json`) + .subscribe((openapi: any) => { + const objs = this.apiObjectModelAdapter(openapi); + const data = this.apiPathModelAdapter(openapi); + this.data.next(data); + this.objs.next(objs); + }); + + } + + /** + * Generate a list of object from the OpenAPi GNS3 documentation with the GET path to query it + * @param openapi GNS3 openapi json data + * @private + */ + private apiObjectModelAdapter(openapi: any): IApiObject[] { + + function haveGetMethod(path: string): boolean { + const obj = openapi.paths[path]; + if (obj) { + const methods = Object.keys(obj); + return methods.includes("get"); + } else { + return false; + } + } + + function extractId(originalPath: string): IApiObject { + const d = originalPath.split('/'); + + const name = d.pop(); + const path = d.join('/'); + + return {name, path}; + } + + const keys = Object.keys(openapi.paths); + return keys + .filter((path: string) => path.match(this.finalBracketIdRegex)) + .filter(haveGetMethod) + .map(extractId) + .filter((object) => haveGetMethod(object.path)); + } + + /** + * Convert OpenAPi json file from GNS3 api documentations to usable information schema + * @param openapi GNS3 openapi definition + * @private + */ + private apiPathModelAdapter(openapi: any): IPathDict[] { + const keys = Object.keys(openapi.paths); + return keys + .map(path => { + const subPaths = path.split('/').filter(elem => !(elem === '' || elem === 'v3')); + return {originalPath: path, path: subPaths.join('/'), subPaths}; + }) + .filter(d => this.allowed.includes(d.subPaths[0])) + .map(path => { + //FIXME + // @ts-ignore + const methods = Object.keys(openapi.paths[path.originalPath]); + return {methods: methods.map(m => m.toUpperCase()), ...path}; + + }) as unknown as IPathDict[]; + } + + /** + * Return availables methods for a path + * @param path '/v3/projects/{project_id} => ['GET', 'POST', 'PUT'] + */ + getMethods(path: string): Observable { + return this.getPath(path) + .pipe( + map((data: IPathDict[]) => { + const availableMethods = new Set(); + data.forEach((p: IPathDict) => { + p.methods.forEach(method => availableMethods.add(method)); + }); + return Array.from(availableMethods) as Methods[]; + }), + ); + } + + /** + * return a list of matching path + * @param path '/v3/projects/{project_id}' + */ + getPath(path: string): Observable { + return this.data + .asObservable() + .pipe( + map((data) => { + const splinted = path + .split('/') + .filter(elem => !(elem === '' || elem === 'v3')); + let remains = data; + splinted.forEach((value, index) => { + if (value === '*') { + return; + } + let matchUrl = remains.filter(val => val.subPaths[index]?.includes(value)); + + if (matchUrl.length === 0) { + matchUrl = remains.filter(val => val.subPaths[index]?.match(this.bracketIdRegex)); + } + remains = matchUrl; + }); + return remains; + }) + ); + } + + private loadLocalInformation() { + const data = JSON.parse(localStorage.getItem('api-definition')); + if (data) { + this.data.next(data); + } + const obj = JSON.parse(localStorage.getItem('api-definition-objs')); + if (obj) { + this.objs.next(obj); + } + } + + /** + * Return all available next child for a given path + * @param path api path + */ + + getPathNextElement(path: string[]): Observable { + + return this.getPath(path.join('/')) + .pipe(map((paths: IPathDict[]) => { + const set = new Set(); + paths.forEach((p) => { + if (p.subPaths[path.length]) { + set.add(p.subPaths[path.length]); + } + }); + + return Array.from(set); + })); + } + + /** + * return all keys which composed a path + * @param path '/v3/projects/7fed4f19-0c45-4461-a45b-93ff11ccdae6/nodes/62f3e04b-a22c-452a-a026-1971122d9d8a + * return: [ + * {key:'project_id', value: '7fed4f19-0c45-4461-a45b-93ff11ccdae6'}, + * {key:'node_id', value: '62f3e04b-a22c-452a-a026-1971122d9d8a'} + * ] + */ + getKeysForPath(path: string): Observable<{ key: string; value: string }[]> { + return this.getPath(path) + .pipe(map((paths: IPathDict[]) => { + const splinted = path + .split('/') + .filter(elem => !(elem === '' || elem === 'v3')); + return paths[0].subPaths.map((elem, index) => { + if (elem.match(this.bracketIdRegex)) { + + return {key: elem, value: splinted[index]}; + } + }); + }), map((values) => { + return values.filter((v) => v !== undefined); + })); + } + + /** + * get the value of specific object with his ID + * @param {server} the server object to query + * @param {key} to query ex :'node_id' + * @param {value} generally the object uuid + * @param {extraParams} somes params like the project_id if you query specific node_id + */ + getListByObjectId(server: Server, key: string, value?: string, extraParams?: IExtraParams[]): Observable { + + const cachedData = this.cache.get(server, key, value, extraParams); + if (cachedData) { + return of(cachedData); + } + + return this.objs.pipe( + map(GetObjectIdHelper.findElementInObjectListFn(key)), + map(GetObjectIdHelper.buildRequestURL(server, value, extraParams)), + switchMap(url => this.httpClient.get(url, {headers: {Authorization: `Bearer ${server.authToken}`}})), + switchMap(GetObjectIdHelper.createResponseObject(key, extraParams, this, server)), + tap(data => this.cache.update(server, key, value, extraParams, data)), + take(1)); + } + + getIdByObjNameFromCache(name: string): IGenericApiObject[] { + return this.cache.searchByName(name); + } +} + + diff --git a/src/app/services/group.service.spec.ts b/src/app/services/group.service.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/services/group.service.ts b/src/app/services/group.service.ts new file mode 100644 index 00000000..c259cb52 --- /dev/null +++ b/src/app/services/group.service.ts @@ -0,0 +1,74 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import {HttpServer} from "./http-server.service"; +import {Server} from "../models/server"; +import {Group} from "../models/groups/group"; +import {User} from "../models/users/user"; +import {Observable} from "rxjs"; +import {Role} from "@models/api/role"; + +@Injectable({ + providedIn: 'root' +}) +export class GroupService { + + constructor( + private httpServer: HttpServer + ) { + } + + getGroups(server: Server) { + return this.httpServer.get(server, '/groups'); + } + + getGroupMember(server: Server, groupId: string) { + return this.httpServer.get(server, `/groups/${groupId}/members`); + } + + addGroup(server: Server, name: string): Observable { + return this.httpServer.post(server, `/groups`, {name}); + } + + delete(server: Server, user_group_id: string) { + return this.httpServer.delete(server, `/groups/${user_group_id}`); + } + + get(server: Server, user_group_id: string) { + return this.httpServer.get(server, `/groups/${user_group_id}`); + } + + addMemberToGroup(server: Server, group: Group, user: User) { + return this.httpServer.put(server, `/groups/${group.user_group_id}/members/${user.user_id}`, {}); + } + + removeUser(server: Server, group: Group, user: User) { + return this.httpServer.delete(server, `/groups/${group.user_group_id}/members/${user.user_id}`); + } + + update(server: Server, group: Group) { + return this.httpServer.put(server, `/groups/${group.user_group_id}`, {name: group.name}); + } + + getGroupRole(server: Server, groupId: string) { + return this.httpServer.get(server, `/groups/${groupId}/roles`); + } + + removeRole(server: Server, group: Group, role: Role) { + return this.httpServer.delete(server, `/groups/${group.user_group_id}/roles/${role.role_id}`); + } + + addRoleToGroup(server: Server, group: Group, role: Role) { + return this.httpServer.put(server, `/groups/${group.user_group_id}/roles/${role.role_id}`, {}); + } +} diff --git a/src/app/services/permissions.service.spec.ts b/src/app/services/permissions.service.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/services/permissions.service.ts b/src/app/services/permissions.service.ts new file mode 100644 index 00000000..ef75c53c --- /dev/null +++ b/src/app/services/permissions.service.ts @@ -0,0 +1,42 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import {Injectable} from '@angular/core'; +import {HttpServer} from "./http-server.service"; +import {Server} from "../models/server"; +import {Permission} from "../models/api/permission"; +import {Observable} from "rxjs/Rx"; + +@Injectable({ + providedIn: 'root' +}) +export class PermissionsService { + + constructor(private httpServer: HttpServer) { + } + + list(server: Server) { + return this.httpServer.get(server, '/permissions'); + } + + add(server: Server, permission: Permission): Observable { + return this.httpServer.post(server, '/permissions', permission); + } + + update(server: Server, permission: Permission): Observable { + return this.httpServer.put(server, `/permissions/${permission.permission_id}`, permission); + } + + delete(server: Server, permission_id: string) { + return this.httpServer.delete(server, `/permissions/${permission_id}`); + } +} diff --git a/src/app/services/role.service.spec.ts b/src/app/services/role.service.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/services/role.service.ts b/src/app/services/role.service.ts new file mode 100644 index 00000000..f4cf41d4 --- /dev/null +++ b/src/app/services/role.service.ts @@ -0,0 +1,55 @@ +/* +* Software Name : GNS3 Web UI +* Version: 3 +* SPDX-FileCopyrightText: Copyright (c) 2022 Orange Business Services +* SPDX-License-Identifier: GPL-3.0-or-later +* +* This software is distributed under the GPL-3.0 or any later version, +* the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt +* or see the "LICENSE" file for more details. +* +* Author: Sylvain MATHIEU, Elise LEBEAU +*/ +import { Injectable } from '@angular/core'; +import {HttpServer} from "./http-server.service"; +import {Server} from "../models/server"; +import {Group} from "../models/groups/group"; +import {Role} from "../models/api/role"; +import {Permission} from "@models/api/permission"; + +@Injectable({ + providedIn: 'root' +}) +export class RoleService { + + constructor(private httpServer: HttpServer) { } + + get(server: Server) { + return this.httpServer.get(server, '/roles'); + } + + delete(server: Server, role_id: string) { + return this.httpServer.delete(server, `/roles/${role_id}`); + } + + create(server: Server, newRole: { name: string; description: string }) { + return this.httpServer.post(server, `/roles`, newRole); + } + + getById(server: Server, roleId: string) { + return this.httpServer.get(server, `/roles/${roleId}`); + } + + update(server: Server, role: Role) { + return this.httpServer.put(server, `/roles/${role.role_id}`, {name: role.name, description: role.description}); + } + + addPermission(server: Server, role: Role, permission: Permission) { + return this.httpServer.put(server, `/roles/${role.role_id}/permissions/${permission.permission_id}`, {}); + + } + + removePermission(server: Server, role: Role, permission: Permission) { + return this.httpServer.delete(server, `/roles/${role.role_id}/permissions/${permission.permission_id}`); + } +} diff --git a/src/app/services/server-version.service.ts b/src/app/services/server-version.service.ts new file mode 100644 index 00000000..34324fd5 --- /dev/null +++ b/src/app/services/server-version.service.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 { Injectable } from '@angular/core'; +import {HttpServer} from "./http-server.service"; +import {Server} from "../models/server"; +import {Observable} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class ServerVersionService { + + constructor(private httpServer: HttpServer) { } + + + public checkServerVersion(server: Server): Observable { + return this.httpServer.get(server, '/version'); + } +} diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts index 01ee827b..e9411983 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/user.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; -import{ Controller } from '../models/controller'; +import { Controller } from '../models/controller'; import { HttpController } from './http-controller.service'; import { User } from '../models/users/user'; +import { Group } from "@models/groups/group"; +import { Permission } from "@models/api/permission"; @Injectable() export class UserService { @@ -10,7 +12,43 @@ export class UserService { private httpController: HttpController ) {} - getInformationAboutLoggedUser(controller:Controller ) { + getInformationAboutLoggedUser(controller: Controller) { return this.httpController.get(controller, '/users/me/'); } + + get(controller: Controller, user_id: string) { + return this.httpController.get(controller, `/users/${user_id}`); + } + + list(controller: Controller) { + return this.httpController.get(controller, '/users'); + } + + add(controller: Controller, user: any): Observable { + return this.httpController.post(controller, `/users`, user); + } + + delete(controller: Controller, user_id: string) { + return this.httpController.delete(controller, `/users/${user_id}`); + } + + update(controller: Controller, user: any): Observable { + return this.httpController.put(controller, `/users/${user.user_id}`, user); + } + + getGroupsByUserId(controller: Controller, user_id: string) { + return this.httpController.get(controller, `/users/${user_id}/groups`); + } + + getPermissionsByUserId(controller: Controller, user_id: string) { + return this.httpController.get(controller, `/users/${user_id}/permissions`); + } + + addPermission(controller: Controller, user_id: string, permission: Permission) { + return this.httpController.put(controller, `/users/${user_id}/permissions/${permission.permission_id}`, {}); + } + + removePermission(controller: Controller, user_id: string, permission: Permission) { + return this.httpController.delete(controller, `/users/${user_id}/permissions/${permission.permission_id}`); + } } diff --git a/src/styles.scss b/src/styles.scss index 2e65a77d..6c8d6f25 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -28,6 +28,7 @@ a.table-link { .snackbar-error { background: #b00020 !important; color: white !important; + white-space: pre-wrap !important; } .mat-dialog-actions { @@ -56,3 +57,14 @@ mat-menu-panel { background-color: grey; color: #ffffff; } + +.permission-tooltip { + max-width: unset !important; + background-color: grey; + color: #ffffff; + background-color: grey; + color: #ffffff; + white-space: pre-line; + font-size: 12px !important; + font-family: monospace; +} diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json index c2d90663..62dc3c77 100644 --- a/src/tsconfig.spec.json +++ b/src/tsconfig.spec.json @@ -4,6 +4,13 @@ "outDir": "../out-tsc/spec", "baseUrl": "./", "target": "es5", + "paths": { + "@components/*": ["app/components/*"], + "@services/*": ["app/services/*"], + "@resolvers/*": ["app/resolvers/*"], + "@filters/*": ["app/filters/*"], + "@models/*": ["app/models/*"] + }, "types": [ "jasmine", "node" diff --git a/tslint.json b/tslint.json index c123a5f1..aaba6e64 100644 --- a/tslint.json +++ b/tslint.json @@ -4,6 +4,8 @@ ], "rules": { "arrow-return-shorthand": true, + "no-implicit-dependencies": false, + "no-submodule-imports": false, "callable-types": true, "class-name": true, "comment-format": [