mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2024-12-30 01:38:50 +00:00
Merge branch 'master-3.0' into enhancement/1353
This commit is contained in:
commit
a79d6c916f
@ -57,6 +57,23 @@ import { ControllerResolve } from './resolvers/controller-resolve';
|
|||||||
import { UserManagementComponent } from './components/user-management/user-management.component';
|
import { UserManagementComponent } from './components/user-management/user-management.component';
|
||||||
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
|
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
|
||||||
import { ImageManagerComponent } from './components/image-manager/image-manager.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 = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -78,6 +95,16 @@ const routes: Routes = [
|
|||||||
{ path: 'help', component: HelpComponent },
|
{ path: 'help', component: HelpComponent },
|
||||||
{ path: 'settings', component: SettingsComponent },
|
{ path: 'settings', component: SettingsComponent },
|
||||||
{ path: 'settings/console', component: ConsoleComponent },
|
{ 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: 'installed-software', component: InstalledSoftwareComponent },
|
||||||
{ path: 'controller/:controller_id/systemstatus', component: SystemStatusComponent, canActivate: [LoginGuard] },
|
{ path: 'controller/:controller_id/systemstatus', component: SystemStatusComponent, canActivate: [LoginGuard] },
|
||||||
|
|
||||||
@ -187,11 +214,72 @@ const routes: Routes = [
|
|||||||
canActivate: [LoginGuard]
|
canActivate: [LoginGuard]
|
||||||
},
|
},
|
||||||
{ path: 'controller/:controller_id/preferences/docker/addtemplate', component: AddDockerTemplateComponent, 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', 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', 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/copy',
|
||||||
|
component: CopyIouTemplateComponent,
|
||||||
|
canActivate: [LoginGuard]
|
||||||
|
},
|
||||||
{ path: 'controller/:controller_id/preferences/iou/addtemplate', component: AddIouTemplateComponent, 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,
|
component: WebConsoleFullWindowComponent,
|
||||||
canActivate: [LoginGuard]
|
canActivate: [LoginGuard]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'user_management',
|
|
||||||
component: UserManagementComponent
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
component: PageNotFoundComponent,
|
component: PageNotFoundComponent,
|
||||||
@ -231,4 +315,5 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {}
|
export class AppRoutingModule {
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* tslint:disable */
|
||||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { OverlayModule } from '@angular/cdk/overlay';
|
import { OverlayModule } from '@angular/cdk/overlay';
|
||||||
import { CdkTableModule } from '@angular/cdk/table';
|
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 { 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 { 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 { 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 { 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 { 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';
|
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 { LoginComponent } from './components/login/login.component';
|
||||||
import { LoginService } from './services/login.service';
|
import { LoginService } from './services/login.service';
|
||||||
import { HttpRequestsInterceptor } from './interceptors/http.interceptor';
|
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 { UserService } from './services/user.service';
|
||||||
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
|
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 { ImageManagerComponent } from './components/image-manager/image-manager.component';
|
||||||
import { AddImageDialogComponent } from './components/image-manager/add-image-dialog/add-image-dialog.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';
|
import { DeleteAllImageFilesDialogComponent } from './components/image-manager/deleteallfiles-dialog/deleteallfiles-dialog.component';
|
||||||
@ -470,6 +512,47 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
|
|||||||
EditNetworkConfigurationDialogComponent,
|
EditNetworkConfigurationDialogComponent,
|
||||||
UserManagementComponent,
|
UserManagementComponent,
|
||||||
ProjectReadmeComponent,
|
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,
|
ImageManagerComponent,
|
||||||
AddImageDialogComponent,
|
AddImageDialogComponent,
|
||||||
DeleteAllImageFilesDialogComponent,
|
DeleteAllImageFilesDialogComponent,
|
||||||
@ -489,6 +572,8 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
|
|||||||
NgxElectronModule,
|
NgxElectronModule,
|
||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
MatSidenavModule,
|
MatSidenavModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatMenuModule,
|
||||||
ResizableModule,
|
ResizableModule,
|
||||||
DragAndDropModule,
|
DragAndDropModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
@ -496,11 +581,14 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
|
|||||||
MATERIAL_IMPORTS,
|
MATERIAL_IMPORTS,
|
||||||
NgCircleProgressModule.forRoot(),
|
NgCircleProgressModule.forRoot(),
|
||||||
OverlayModule,
|
OverlayModule,
|
||||||
|
MatSlideToggleModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
{ provide: ErrorHandler, useClass: ToasterErrorHandler },
|
{provide: ErrorHandler, useClass: ToasterErrorHandler},
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpRequestsInterceptor, multi: true },
|
{provide: HTTP_INTERCEPTORS, useClass: HttpRequestsInterceptor, multi: true},
|
||||||
D3Service,
|
D3Service,
|
||||||
VersionService,
|
VersionService,
|
||||||
ProjectService,
|
ProjectService,
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
<div class="title">
|
||||||
|
<h3>Add Role To group: {{data.group.name}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<mat-form-field class="input-field">
|
||||||
|
<mat-label>Search user </mat-label>
|
||||||
|
<input matInput type="text" [(ngModel)]="searchText" (keydown)="onSearch()">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="userList" *ngFor="let role of displayedRoles | async">
|
||||||
|
<div>{{role.name}}</div>
|
||||||
|
<mat-icon (click)="addRole(role)" *ngIf="!loading">add</mat-icon>
|
||||||
|
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<Role[]>([]);
|
||||||
|
displayedRoles = new BehaviorSubject<Role[]>([]);
|
||||||
|
|
||||||
|
searchText: string;
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
constructor(private dialog: MatDialogRef<AddRoleToGroupComponent>,
|
||||||
|
@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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<div class="title">
|
||||||
|
<h3>Add User To group: {{data.group.name}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<mat-form-field class="input-field">
|
||||||
|
<mat-label>Search user </mat-label>
|
||||||
|
<input matInput type="text" [(ngModel)]="searchText" (keydown)="onSearch()">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="userList" *ngFor="let user of displayedUsers | async">
|
||||||
|
<div>{{user.username}}</div>
|
||||||
|
<div>{{user.email}}</div>
|
||||||
|
<mat-icon (click)="addUser(user)" *ngIf="!loading">add</mat-icon>
|
||||||
|
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<User[]>([]);
|
||||||
|
displayedUsers = new BehaviorSubject<User[]>([]);
|
||||||
|
|
||||||
|
searchText: string;
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
constructor(private dialog: MatDialogRef<AddUserToGroupDialogComponent>,
|
||||||
|
@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;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
<div class="content">
|
||||||
|
<div class="default-header">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<a
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="Back to group management"
|
||||||
|
matTooltipClass="custom-tooltip"
|
||||||
|
[routerLink]="['/server', server.id, 'management', 'groups']">
|
||||||
|
<mat-icon aria-label="Back to group management">keyboard_arrow_left</mat-icon>
|
||||||
|
</a>
|
||||||
|
<h1 class="col">Groups {{group.name}} details</h1>
|
||||||
|
</div>
|
||||||
|
<mat-tab-group>
|
||||||
|
<mat-tab label="Details" class="details">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Group name:</mat-label>
|
||||||
|
<input matInput type="text" [ngModel]="group.name">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<mat-checkbox [checked]="group.is_builtin" disabled>Is build in</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary"
|
||||||
|
[disabled]="!editGroupForm.valid">
|
||||||
|
Update Group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Creation date: {{group.created_at}}</div>
|
||||||
|
<div>Last update Date: {{group.updated_at}}</div>
|
||||||
|
<div>UUID: {{group.user_group_id}}</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
<mat-tab label="Members">
|
||||||
|
<div class="members">
|
||||||
|
<div>
|
||||||
|
<mat-icon (click)="openAddUserDialog()" class="clickable">person_add</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div class="search">
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput placeholder="filter members" [(ngModel)]="searchMembers"/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div *ngFor="let user of members | membersFilter: searchMembers | paginator: pageEvent">
|
||||||
|
<a href="/server/{{server.id}}/management/users/{{user.user_id}}">
|
||||||
|
<div>{{user.username}}</div>
|
||||||
|
</a>
|
||||||
|
<mat-icon class="clickable" (click)="openRemoveUserDialog(user)">delete</mat-icon>
|
||||||
|
</div>
|
||||||
|
<mat-paginator [length]="members.length" (page)="pageEvent = $event"
|
||||||
|
[pageSizeOptions]="[5, 20, 50, 100]"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
<mat-tab label="Roles">
|
||||||
|
<div><button mat-button (click)="openAddRoleDialog()" ><mat-icon>group_add</mat-icon></button></div>
|
||||||
|
<div *ngFor="let role of roles" class="roles">
|
||||||
|
<div>{{role.name}}</div>
|
||||||
|
<div>
|
||||||
|
<button mat-button (click)="openRemoveRoleDialog(role)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
152
src/app/components/group-details/group-details.component.ts
Normal file
152
src/app/components/group-details/group-details.component.ts
Normal file
@ -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>(AddRoleToGroupComponent,
|
||||||
|
{
|
||||||
|
width: '700px', height: '500px',
|
||||||
|
data: {server: this.server, group: this.group}
|
||||||
|
})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe(() => {
|
||||||
|
this.reloadRoles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
openAddUserDialog() {
|
||||||
|
this.dialog
|
||||||
|
.open<AddUserToGroupDialogComponent>(AddUserToGroupDialogComponent,
|
||||||
|
{
|
||||||
|
width: '700px', height: '500px',
|
||||||
|
data: {server: this.server, group: this.group}
|
||||||
|
})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe(() => {
|
||||||
|
this.reloadMembers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openRemoveUserDialog(user: User) {
|
||||||
|
this.dialog.open<RemoveToGroupDialogComponent>(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>(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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
src/app/components/group-details/members-filter.pipe.ts
Normal file
32
src/app/components/group-details/members-filter.pipe.ts
Normal file
@ -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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
src/app/components/group-details/paginator.pipe.ts
Normal file
41
src/app/components/group-details/paginator.pipe.ts
Normal file
@ -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<T>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
<div class="header">
|
||||||
|
<div>Confirm ?</div>
|
||||||
|
<div>Removing: {{data.name}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="button">
|
||||||
|
<button mat-button mat-raised-button color="primary" (click)="onCancel()">No, cancel</button>
|
||||||
|
<button mat-button mat-raised-button color="warn"(click)="onConfirm()">Yes, remove</button>
|
||||||
|
</div>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
@ -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<RemoveToGroupDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { name: string }) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirm() {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -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<RemoveUserToGroupDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { user: User }) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirm() {
|
||||||
|
this.dialogRef.close(this.data.user);
|
||||||
|
}
|
||||||
|
}
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
<h1 mat-dialog-title>Create new group</h1>
|
||||||
|
<form [formGroup]="groupNameForm" class="file-name-form">
|
||||||
|
<mat-form-field class="file-name-form-field">
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
(keydown)="onKeyDown($event)"
|
||||||
|
type="text"
|
||||||
|
formControlName="groupName"
|
||||||
|
[ngClass]="{ 'is-invalid': form.groupName?.errors }"
|
||||||
|
placeholder="Please enter group name"
|
||||||
|
/>
|
||||||
|
<mat-error *ngIf="form.groupName?.touched && form.groupName?.errors && form.groupName?.errors.required"
|
||||||
|
>Group name is required</mat-error
|
||||||
|
>
|
||||||
|
<mat-error *ngIf="form.groupName?.errors && form.groupName?.errors.invalidName"
|
||||||
|
>Group name is incorrect</mat-error
|
||||||
|
>
|
||||||
|
<mat-error *ngIf="form.groupName?.errors && form.groupName?.errors.projectExist"
|
||||||
|
>Group with this name exists</mat-error
|
||||||
|
>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h5>Add users to group: </h5>
|
||||||
|
<mat-form-field class="input-field">
|
||||||
|
<mat-label>Users</mat-label>
|
||||||
|
<input type="text"
|
||||||
|
matInput
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
[formControl]="autocompleteControl">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete"[displayWith]="displayFn" (optionSelected)='selectedUser($event.option.value)'>
|
||||||
|
<mat-option *ngFor="let user of filteredUsers | async" [value]="user" >
|
||||||
|
{{user.username}} - {{user.email}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div class="users">
|
||||||
|
<div *ngFor="let user of usersToAdd">
|
||||||
|
<div class="userList">
|
||||||
|
<div>{{user.username}}</div>
|
||||||
|
<div>{{user.email}}</div>
|
||||||
|
<mat-icon (click)="delUser(user)">delete</mat-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
|
||||||
|
<button mat-button (click)="onAddClick()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Add group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
.file-name-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-snackbar {
|
||||||
|
background: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userList {
|
||||||
|
display: flex;
|
||||||
|
margin: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users {
|
||||||
|
display: flex;
|
||||||
|
height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-div {
|
||||||
|
float: right;
|
||||||
|
}
|
@ -0,0 +1,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<User> = new Set([]);
|
||||||
|
filteredUsers: Observable<User[]>
|
||||||
|
loading = false;
|
||||||
|
autocompleteControl = new FormControl();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(private dialogRef: MatDialogRef<AddGroupDialogComponent>,
|
||||||
|
@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 : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,8 @@
|
|||||||
|
<h1 mat-dialog-title>Are you sure to delete group named: </h1>
|
||||||
|
<p *ngFor="let group of data.groups">{{group.name}}</p>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onCancel()" color="accent">No, cancel</button>
|
||||||
|
<button mat-button (click)="onDelete()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Yes, delete!
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,6 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
@ -0,0 +1,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<DeleteGroupDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { groups: Group[] }) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete() {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
<div class="content" *ngIf="isReady; else loading">
|
||||||
|
<div class="default-header">
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="col">Groups management</h1>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="onDelete(selection.selected)" class="add-group-button" [disabled]="selection.selected.length == 0">
|
||||||
|
Delete selected groups
|
||||||
|
</button>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="addGroup()" class="add-group-button">
|
||||||
|
Add group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<mat-form-field class="full-width">
|
||||||
|
<input matInput placeholder="Search by name" [(ngModel)]="searchText" [ngModelOptions]="{ standalone: true }" />
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="default-content">
|
||||||
|
<table mat-table [dataSource]="dataSource | groupFilter: searchText" class="mat-elevation-z8" matSort #groupsSort="matSort">
|
||||||
|
|
||||||
|
<ng-container matColumnDef="select" >
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="small-col">
|
||||||
|
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row" class="small-col">
|
||||||
|
<mat-checkbox (click)="$event.stopPropagation()"
|
||||||
|
(change)="$event ? selection.toggle(row) : null"
|
||||||
|
[checked]="selection.isSelected(row)">
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name</th>
|
||||||
|
<td mat-cell *matCellDef="let element"><a class="table-link" routerLink="/server/{{server.id}}/management/groups/{{element.user_group_id}}">{{element.name}}</a> </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="created_at">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Creation date</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.created_at}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-container matColumnDef="updated_at">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Last update</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.updated_at}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-container matColumnDef="is_builtin">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> is build in</th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.is_builtin}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="delete">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> </th>
|
||||||
|
<td mat-cell *matCellDef="let element"><button mat-button (click)="onDelete([element])"><mat-icon>delete</mat-icon></button></td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<mat-paginator #groupsPaginator="matPaginator"
|
||||||
|
[pageSizeOptions]="[5, 10, 20]"
|
||||||
|
showFirstLastButtons
|
||||||
|
aria-label="Select page">
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #loading>
|
||||||
|
<div>
|
||||||
|
<mat-spinner class="loader"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -0,0 +1,26 @@
|
|||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 940px;
|
||||||
|
margin-left: -470px;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-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;
|
||||||
|
}
|
@ -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<MatPaginator>;
|
||||||
|
@ViewChildren('groupsSort') groupsSort: QueryList<MatSort>;
|
||||||
|
|
||||||
|
public displayedColumns = ['select', 'name', 'created_at', 'updated_at', 'is_builtin', 'delete'];
|
||||||
|
selection = new SelectionModel<Group>(true, []);
|
||||||
|
groups: Group[];
|
||||||
|
dataSource = new MatTableDataSource<Group>();
|
||||||
|
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 <MatPaginator>) =>
|
||||||
|
{
|
||||||
|
this.dataSource.paginator = comps.first;
|
||||||
|
});
|
||||||
|
this.groupsSort.changes.subscribe((comps: QueryList<MatSort>) => {
|
||||||
|
this.dataSource.sort = comps.first;
|
||||||
|
});
|
||||||
|
this.dataSource.sortingDataAccessor = (item, property) => {
|
||||||
|
switch (property) {
|
||||||
|
case 'name':
|
||||||
|
return item[property] ? item[property].toLowerCase() : '';
|
||||||
|
default:
|
||||||
|
return item[property];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllSelected() {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
const numRows = this.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`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
9
src/app/components/management/management.component.html
Normal file
9
src/app/components/management/management.component.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<nav mat-tab-nav-bar>
|
||||||
|
<a mat-tab-link *ngFor="let link of links"
|
||||||
|
(click)="activeLink = link"
|
||||||
|
routerLink="./{{link}}"
|
||||||
|
routerLinkActive
|
||||||
|
[active]="rla.isActive"
|
||||||
|
#rla="routerLinkActive">{{link.charAt(0).toUpperCase() + link.slice(1)}} </a>
|
||||||
|
</nav>
|
||||||
|
<router-outlet></router-outlet>
|
40
src/app/components/management/management.component.ts
Normal file
40
src/app/components/management/management.component.ts
Normal file
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
<button [ngClass]="{allow: action === 'ALLOW', deny: action === 'DENY'}"
|
||||||
|
mat-button
|
||||||
|
[disabled]="disabled"
|
||||||
|
(click)="change()">
|
||||||
|
{{action}}
|
||||||
|
</button>
|
@ -0,0 +1,8 @@
|
|||||||
|
.allow {
|
||||||
|
background-color: green;
|
||||||
|
border-radius: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deny {
|
||||||
|
background-color: darkred;
|
||||||
|
}
|
@ -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<PermissionActions>();
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
change() {
|
||||||
|
this.action === PermissionActions.DENY ? this.action = PermissionActions.ALLOW : this.action = PermissionActions.DENY;
|
||||||
|
this.update.emit(this.action);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<div class="box-border">
|
||||||
|
<div *ngIf="edit; else add">
|
||||||
|
<div class="edit-mode">
|
||||||
|
<div class="information-box">
|
||||||
|
<div>
|
||||||
|
<app-path-auto-complete
|
||||||
|
[server]="server"
|
||||||
|
(update)="permission.path = $event"></app-path-auto-complete>
|
||||||
|
</div>
|
||||||
|
<div class="methods">
|
||||||
|
<app-action-button
|
||||||
|
[disabled]="false"
|
||||||
|
[action]="permission.action"></app-action-button>
|
||||||
|
<div *ngFor="let method of apiInformation.getMethods(permission.path) | async">
|
||||||
|
<app-method-button
|
||||||
|
[name]="method"
|
||||||
|
[disabled]="false"
|
||||||
|
(update)="updateMethod($event)"></app-method-button>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
[(ngModel)]="permission.description"
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
placeholder="Description"/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-box">
|
||||||
|
<button mat-button (click)="reset()">
|
||||||
|
<mat-icon>cancel</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="save()">
|
||||||
|
<mat-icon>done</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #add>
|
||||||
|
<div class="not-edit">
|
||||||
|
<button mat-button (click)="edit = true">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<Permission> => {
|
||||||
|
return of(permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
let message: string;
|
||||||
|
|
||||||
|
toasterService.success = (m: string) => {
|
||||||
|
message = m;
|
||||||
|
};
|
||||||
|
|
||||||
|
comp.save();
|
||||||
|
const p = comp.permission;
|
||||||
|
|
||||||
|
tick();
|
||||||
|
expect(message).toBeTruthy();
|
||||||
|
expect(p.methods).toEqual([]);
|
||||||
|
expect(p.action).toEqual(PermissionActions.ALLOW);
|
||||||
|
expect(p.description).toEqual('');
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('Should throw error on rejected permission', fakeAsync(() => {
|
||||||
|
const comp = TestBed.inject(AddPermissionLineComponent);
|
||||||
|
const permissionService = TestBed.inject(PermissionsService);
|
||||||
|
const toasterService = TestBed.inject(ToasterService);
|
||||||
|
comp.permission.methods = [Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE];
|
||||||
|
comp.permission.path = "/test/path";
|
||||||
|
comp.permission.action = PermissionActions.DENY;
|
||||||
|
comp.permission.description = "john doe is here";
|
||||||
|
|
||||||
|
let errorMessage: string;
|
||||||
|
|
||||||
|
permissionService.add = (server: Server, permission: Permission): Observable<Permission> => {
|
||||||
|
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();
|
||||||
|
}));
|
||||||
|
});
|
@ -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<void>();
|
||||||
|
permission: Permission = {
|
||||||
|
action: PermissionActions.ALLOW,
|
||||||
|
description: "",
|
||||||
|
methods: [],
|
||||||
|
path: "/"
|
||||||
|
};
|
||||||
|
edit = false;
|
||||||
|
|
||||||
|
constructor(public apiInformation: ApiInformationService,
|
||||||
|
private permissionService: PermissionsService,
|
||||||
|
private toasterService: ToasterService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateMethod(data: { name: Methods; enable: boolean }) {
|
||||||
|
const set = new Set(this.permission.methods);
|
||||||
|
if (data.enable) {
|
||||||
|
set.add(data.name);
|
||||||
|
} else {
|
||||||
|
set.delete(data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.permission.methods = Array.from(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.permission = {
|
||||||
|
action: PermissionActions.ALLOW,
|
||||||
|
description: "",
|
||||||
|
methods: [],
|
||||||
|
path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
this.edit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.permissionService.add(this.server, this.permission)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.toasterService.success(`permission was created`);
|
||||||
|
this.reset();
|
||||||
|
}, (error: HttpErrorResponse) => {
|
||||||
|
this.toasterService.error(`
|
||||||
|
${error.message}
|
||||||
|
${error.error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<div class="path">
|
||||||
|
<div>Path: /</div>
|
||||||
|
<div *ngFor="let p of path.getDisplayPath()">{{p}}/</div>
|
||||||
|
<div class="path-edit-line">
|
||||||
|
<div>
|
||||||
|
<div *ngIf="mode === 'SELECT'">
|
||||||
|
<mat-select (valueChange)="valueChanged($event)" class="edit-area">
|
||||||
|
<mat-option *ngFor="let value of values" value="{{value}}">{{value}}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="mode === 'COMPLETE'">
|
||||||
|
<input matInput
|
||||||
|
autofocus
|
||||||
|
class="complete edit-area"
|
||||||
|
aria-label="find"
|
||||||
|
[(ngModel)]="completeField"
|
||||||
|
[matAutocomplete]="auto">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete">
|
||||||
|
<mat-option [value]="'*'">*</mat-option>
|
||||||
|
<mat-option *ngFor="let data of completeData.data | filterComplete: completeField"
|
||||||
|
[value]="data.name">
|
||||||
|
<span>{{data.name}}</span>
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="command-button">
|
||||||
|
<mat-icon (click)="removePrevious()" *ngIf="!path.isEmpty()">cancel</mat-icon>
|
||||||
|
<mat-icon (click)="getNext()" *ngIf="!this.mode">add_circle_outline</mat-icon>
|
||||||
|
<mat-icon
|
||||||
|
matTooltip="validate data"
|
||||||
|
(click)="validComplete()"
|
||||||
|
*ngIf="this.mode === 'COMPLETE'">check_circle
|
||||||
|
</mat-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<string>();
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<div class="description">
|
||||||
|
<div>confirm deleting permission:</div>
|
||||||
|
<div>{{data.permission_id}}</div>
|
||||||
|
<div>{{data.path}}</div>
|
||||||
|
<div>{{data.methods.join(',')}}</div>
|
||||||
|
<div>{{data.action}}</div>
|
||||||
|
<div>{{data.description}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="button">
|
||||||
|
<button mat-button mat-raised-button (click)="cancel()">No, cancel</button>
|
||||||
|
<button mat-button mat-raised-button color="primary" (click)="confirm()">Yes, remove</button>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<DeletePermissionDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: Permission) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.dialog.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
this.dialog.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -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<IGenericApiObject[]> => {
|
||||||
|
if (key === 'project_id') {
|
||||||
|
return of([{id: '1111-2222-3333', name: 'myProject'}]);
|
||||||
|
}
|
||||||
|
if (key === 'node_id') {
|
||||||
|
return of([{id: '2222-2222-2222', name: 'node1'}]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
const 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');
|
||||||
|
}));
|
||||||
|
});
|
@ -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<string> {
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<button
|
||||||
|
[disabled]="disabled"
|
||||||
|
[ngClass]="{enable: enable, disabled: disabled}"
|
||||||
|
(click)="change()"
|
||||||
|
mat-button>
|
||||||
|
{{name}}
|
||||||
|
</button>
|
@ -0,0 +1,10 @@
|
|||||||
|
:host {
|
||||||
|
padding: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enable {
|
||||||
|
color: green !important;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
color: dimgrey;
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@ -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});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
<div class="permission">
|
||||||
|
<div class="action-button-bar">
|
||||||
|
<div>
|
||||||
|
<app-action-button
|
||||||
|
[action]="permission.action"
|
||||||
|
[disabled]="!isEditable"
|
||||||
|
(update)="permission.action = $event"></app-action-button>
|
||||||
|
</div>
|
||||||
|
<div class="methods">
|
||||||
|
<div *ngFor="let method of apiInformation.getMethods(permission.path) | async">
|
||||||
|
<app-method-button
|
||||||
|
[name]="method"
|
||||||
|
[disabled]="!isEditable"
|
||||||
|
[enable]="permission.methods.includes(method)"
|
||||||
|
(update)="onMethodUpdate($event)"></app-method-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
[matTooltip]="permission.path | displayPath: server | async"
|
||||||
|
matTooltipClass="custom-tooltip">
|
||||||
|
{{permission.path | displayPath: server | async}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field class="permission-input" appearance="none">
|
||||||
|
<input
|
||||||
|
[(ngModel)]="permission.description"
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
placeholder="Description"
|
||||||
|
[matTooltip]="permission.description"
|
||||||
|
matTooltipClass="custom-tooltip"
|
||||||
|
[readonly]="!isEditable"/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-bar">
|
||||||
|
<div>
|
||||||
|
<button mat-button matTooltip="Edit permission" (click)="isEditable = true" *ngIf="!isEditable">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-button matTooltip="Delete permission" (click)="onDelete()" *ngIf="!isEditable">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button mat-button matTooltip="Save Changes" (click)="onSave()" *ngIf="isEditable" color="primary">
|
||||||
|
<mat-icon>check_circle</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-button matTooltip="Cancel Changes" color="warn" (click)="onCancel()" *ngIf="isEditable">
|
||||||
|
<mat-icon>cancel</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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<void>();
|
||||||
|
|
||||||
|
constructor(public apiInformation: ApiInformationService,
|
||||||
|
private permissionService: PermissionsService,
|
||||||
|
private toasterService: ToasterService,
|
||||||
|
private dialog: MatDialog) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onDelete() {
|
||||||
|
this.dialog.open<DeletePermissionDialogComponent>(DeletePermissionDialogComponent,
|
||||||
|
{width: '700px', height: '500px', data: this.permission})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe((confirm: boolean) => {
|
||||||
|
if (confirm) {
|
||||||
|
this.permissionService.delete(this.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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
<div class="content" *ngIf="isReady; else loading">
|
||||||
|
<div class="add">
|
||||||
|
<app-add-permission-line
|
||||||
|
[server]="server"
|
||||||
|
(addPermissionEvent)="refresh()"></app-add-permission-line>
|
||||||
|
</div>
|
||||||
|
<div class="permission-content default-content">
|
||||||
|
<input type="text"
|
||||||
|
matInput
|
||||||
|
name="typeInput"
|
||||||
|
placeholder="Search by name"
|
||||||
|
[(ngModel)]="searchPermissions"
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
class="permission-filter"
|
||||||
|
(input)="changeAutocomplete($event.target.value)">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
|
||||||
|
<mat-option *ngFor="let option of filteredOptions | filterComplete: searchPermissions" [value]="option">
|
||||||
|
{{option.name}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
|
||||||
|
<div *ngFor="let permission of permissions | permissionsFilter: searchPermissions?.id | paginator: pageEvent">
|
||||||
|
<app-permission-add-edit-line
|
||||||
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
|
(update)="refresh()"></app-permission-add-edit-line>
|
||||||
|
</div>
|
||||||
|
<mat-paginator [length]="permissions.length" (page)="pageEvent = $event"
|
||||||
|
[pageSizeOptions]="[5, 20, 50, 100]"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #loading>
|
||||||
|
<div>
|
||||||
|
<mat-spinner class="loader"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<h1 mat-dialog-title>Create new role</h1>
|
||||||
|
<form [formGroup]="roleNameForm" class="file-name-form">
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
formControlName="name"
|
||||||
|
[ngClass]="{ 'is-invalid': form.name?.errors }"
|
||||||
|
placeholder="Please enter role name"/>
|
||||||
|
<mat-error *ngIf="form.name?.touched && form.name?.errors && form.name?.errors.required">
|
||||||
|
role name is required
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="form.name?.errors && form.name?.errors.invalidName">
|
||||||
|
Role name is incorrect
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="text"
|
||||||
|
formControlName="description"
|
||||||
|
placeholder="Please enter a description"/>
|
||||||
|
</mat-form-field>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
|
||||||
|
<button mat-button (click)="onAddClick()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Add Role
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,7 @@
|
|||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-snackbar {
|
||||||
|
background: #2196f3;
|
||||||
|
}
|
@ -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<AddRoleDialogComponent>,
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<h1 mat-dialog-title>Are you sure to delete role named: </h1>
|
||||||
|
<p *ngFor="let role of data.roles">{{role.name}}</p>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onCancel()" color="accent">No, cancel</button>
|
||||||
|
<button mat-button (click)="onDelete()" tabindex="2" class="add-project-button" mat-raised-button color="primary">
|
||||||
|
Yes, delete!
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,6 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
@ -0,0 +1,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<DeleteRoleDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { roles: Role[] }) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete() {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<div [matTooltip]="getToolTip()"
|
||||||
|
matTooltipClass="permission-tooltip"
|
||||||
|
class="box" [ngClass]="{allow: permission.action === 'ALLOW', left: side === 'LEFT'}">
|
||||||
|
<button *ngIf="side === 'RIGHT'" mat-button (click)="onClick()">
|
||||||
|
<mat-icon>keyboard_arrow_left</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div class="content">
|
||||||
|
<div class="center">{{permission.methods.join(",")}}</div>
|
||||||
|
<div class="center">{{permission.path | displayPath: server | async}}</div>
|
||||||
|
</div>
|
||||||
|
<button *ngIf="side === 'LEFT'" mat-button (click)="onClick()">
|
||||||
|
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<div>
|
||||||
|
<div class="button">
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
mat-raised-button
|
||||||
|
(click)="close()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button *ngIf="data.add.length > 0 || data.remove.length > 0"
|
||||||
|
mat-button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
(click)="update()">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="change" *ngIf="data.add.length > 0 || data.remove.length > 0; else nothingTodo">
|
||||||
|
<p></p>
|
||||||
|
<div class="title" *ngIf="data.add.length > 0">Permission to Add:</div>
|
||||||
|
<div *ngFor="let permission of data.add">
|
||||||
|
<app-editable-permission [permission]="permission"></app-editable-permission>
|
||||||
|
</div>
|
||||||
|
<div class="title" *ngIf="data.remove.length > 0">Permission to Remove:</div>
|
||||||
|
<div *ngFor="let permission of data.remove">
|
||||||
|
<app-editable-permission [permission]="permission"></app-editable-permission>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #nothingTodo>
|
||||||
|
<div class="noChange">
|
||||||
|
No change
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<PermissionEditorValidateDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { add: Permission[], remove: Permission[] }) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Allow:
|
||||||
|
</div>
|
||||||
|
<div class="box allow"></div>
|
||||||
|
<div>
|
||||||
|
Deny:
|
||||||
|
</div>
|
||||||
|
<div class="box deny"></div>
|
||||||
|
</div>
|
||||||
|
<input type="text"
|
||||||
|
matInput
|
||||||
|
name="typeInput"
|
||||||
|
placeholder="Search by name"
|
||||||
|
[(ngModel)]="searchPermissions"
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
class="permission-filter"
|
||||||
|
(input)="changeAutocomplete($event.target.value)">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
|
||||||
|
<mat-option *ngFor="let option of filteredOptions | filterComplete: searchPermissions" [value]="option">
|
||||||
|
{{option.name}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
mat-raised-button
|
||||||
|
(click)="reset()">Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
mat-raised-button
|
||||||
|
(click)="update()"
|
||||||
|
color="primary">Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="editor">
|
||||||
|
<div class="column">
|
||||||
|
<div class="title">Owned</div>
|
||||||
|
<app-editable-permission
|
||||||
|
[side]="'LEFT'"
|
||||||
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
|
(click)="remove(permission)"
|
||||||
|
*ngFor="let permission of ownedArray | permissionsFilter: searchPermissions?.id | paginator: pageEventOwned">
|
||||||
|
|
||||||
|
</app-editable-permission>
|
||||||
|
<mat-paginator [length]="ownedArray.length" (page)="pageEventOwned = $event"
|
||||||
|
[pageSizeOptions]="[5, 20, 50, 100]"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
<mat-divider [vertical]="true"></mat-divider>
|
||||||
|
<div class="column">
|
||||||
|
<div class="title">Available</div>
|
||||||
|
<app-editable-permission
|
||||||
|
[side]="'RIGHT'"
|
||||||
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
|
(click)="add(permission)"
|
||||||
|
*ngFor="let permission of availableArray | permissionsFilter: searchPermissions?.id | paginator: pageEventAvailable">
|
||||||
|
|
||||||
|
</app-editable-permission>
|
||||||
|
<mat-paginator [length]="availableArray.length" (page)="pageEventAvailable = $event"
|
||||||
|
[pageSizeOptions]="[5, 20, 50, 100]"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -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;
|
||||||
|
}
|
@ -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<Permission>;
|
||||||
|
available: Set<Permission>;
|
||||||
|
searchPermissions: any;
|
||||||
|
filteredOptions: IGenericApiObject[];
|
||||||
|
pageEventOwned: PageEvent | undefined;
|
||||||
|
pageEventAvailable: PageEvent | undefined;
|
||||||
|
|
||||||
|
@Input() server: Server;
|
||||||
|
@Input() ownedPermissions: Permission[];
|
||||||
|
@Input() availablePermissions: Permission[];
|
||||||
|
@Output() updatedPermissions: EventEmitter<any> = 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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
<div class="content">
|
||||||
|
<div class="default-header">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<a
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="Back to role management"
|
||||||
|
matTooltipClass="custom-tooltip"
|
||||||
|
[routerLink]="['/server', server.id, 'management', 'roles']">
|
||||||
|
<mat-icon aria-label="Back to role management">keyboard_arrow_left</mat-icon>
|
||||||
|
</a>
|
||||||
|
<h1 class="col">Role {{role.name}} details</h1>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="details">
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Role name:</mat-label>
|
||||||
|
<input matInput type="text" [ngModel]="role.name">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Description:</mat-label>
|
||||||
|
<input matInput type="text" [ngModel]="role.description">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>Creation date: {{role.created_at}}</div>
|
||||||
|
<div>Last update Date: {{role.updated_at}}</div>
|
||||||
|
<div>UUID: {{role.role_id}}</div>
|
||||||
|
<div>
|
||||||
|
<mat-checkbox [checked]="role.is_builtin" disabled>Is build in</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions class="button-div">
|
||||||
|
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary"
|
||||||
|
[disabled]="!editRoleForm.valid">
|
||||||
|
Update Role
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-divider [vertical]="true"></mat-divider>
|
||||||
|
<div class="permissions">
|
||||||
|
<div class="header">
|
||||||
|
<div>Permissions</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
mat-button
|
||||||
|
[routerLink]="['/server', server.id, 'management', 'roles', role.role_id, 'permissions']">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-editable-permission
|
||||||
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
|
*ngFor="let permission of role.permissions">
|
||||||
|
</app-editable-permission>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user