Merge pull request #1359 from GNS3/enhancement/1354

Enhancement/1354
This commit is contained in:
Jeremy Grossmann 2022-07-26 23:40:10 +02:00 committed by GitHub
commit 73bc8cd4b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 291 additions and 33 deletions

View File

@ -92,9 +92,9 @@ const routes: Routes = [
canActivate: [LoginGuard],
resolve: { controller: ControllerResolve },
},
{ path: 'help', component: HelpComponent },
{ path: 'settings', component: SettingsComponent },
{ path: 'settings/console', component: ConsoleComponent },
{ path: 'controller/:controller_id/help', component: HelpComponent },
{ path: 'controller/:controller_id/settings', component: SettingsComponent },
{ path: 'controller/:controller_id/settings/console', component: ConsoleComponent },
{
path: 'controller/:controller_id/management/users/:user_id',
component: UserDetailComponent,

View File

@ -322,6 +322,7 @@ import { DeleteAllImageFilesDialogComponent } from './components/image-manager/d
import { UploadingProcessbarComponent } from './common/uploading-processbar/uploading-processbar.component';
import { ExportPortableProjectComponent } from './components/export-portable-project/export-portable-project.component';
import { NodesMenuConfirmationDialogComponent } from './components/project-map/nodes-menu/nodes-menu-confirmation-dialog/nodes-menu-confirmation-dialog.component';
import { ConfirmationDeleteAllProjectsComponent } from './components/projects/confirmation-delete-all-projects/confirmation-delete-all-projects.component';
@NgModule({
declarations: [
@ -559,6 +560,7 @@ import { NodesMenuConfirmationDialogComponent } from './components/project-map/n
UploadingProcessbarComponent,
ExportPortableProjectComponent,
NodesMenuConfirmationDialogComponent,
ConfirmationDeleteAllProjectsComponent,
],
imports: [
BrowserModule,

View File

@ -998,6 +998,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.exportPortableProjectDialog();
}
}
exportPortableProjectDialog() {
const dialogRef = this.dialog.open(ExportPortableProjectComponent, {
width: '700px',

View File

@ -1,4 +1,4 @@
<h1 mat-dialog-title>Please choose name for exporting project</h1>
<h1 mat-dialog-title>Save project as</h1>
<div class="modal-form-container">
<mat-form-field class="form-field">
@ -8,5 +8,5 @@
<div mat-dialog-actions>
<button mat-button (click)="onCloseClick()" color="accent">Cancel</button>
<button mat-button (click)="onSaveClick()" tabindex="2" mat-raised-button color="primary">Apply</button>
<button mat-button (click)="onSaveClick()" tabindex="2" mat-raised-button color="primary">Save project</button>
</div>

View File

@ -0,0 +1,34 @@
<div *ngIf="!isDelete && !isUsedFiles">
<h1 mat-dialog-title>Do you want delete all projects?</h1>
<div mat-dialog-content>
<p>Your selected projects</p>
<p *ngFor="let file of deleteData?.deleteFilesPaths; let i = index">{{i+1}}. {{file?.filename}}</p>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="deleteAll()">Delete</button>
<button mat-button mat-dialog-close cdkFocusInitial>Cancel</button>
</div>
</div>
<div *ngIf="isDelete && !isUsedFiles">
<h1 align="center" mat-dialog-title>Please wait.</h1>
<div mat-dialog-content align="center">
<mat-spinner color="accent"></mat-spinner>
</div>
</div>
<div *ngIf="isDelete && isUsedFiles">
<div mat-dialog-content>
<div *ngIf="deleteFliesDetails.length > 0">
<h5>Project can't be deleted because image used in one or more template.</h5>
<p *ngFor="let message of deleteFliesDetails; let i = index" [ngClass]="{'deleted-error-text': message?.error?.message}"><span *ngIf="message !=null">{{i+1}}. {{message?.error?.message}}</span></p>
</div>
<div *ngIf="fileNotDeleted.length > 0">
<h5 class="delete-text">{{fileNotDeleted.length}} Projects deleted successfully.</h5>
</div>
</div>
<div mat-dialog-actions align="end">
<button mat-raised-button color="primary" (click)="dialogRef.close(false)">Close</button>
</div>
</div>

View File

@ -0,0 +1,54 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MockedProjectService } from '../../../services/project.service.spec';
import { MockedToasterService } from '../../../services/toaster.service.spec';
import { ProjectService } from '../../../services/project.service';
import { ControllerService } from '../../../services/controller.service';
import { MockedControllerService } from '../../../services/controller.service.spec';
import { ToasterService } from '../../../services/toaster.service';
import { ConfirmationDeleteAllProjectsComponent } from './confirmation-delete-all-projects.component';
describe('ConfirmationDeleteAllProjectsComponent', () => {
let component: ConfirmationDeleteAllProjectsComponent;
let fixture: ComponentFixture<ConfirmationDeleteAllProjectsComponent>;
let mockedControllerService = new MockedControllerService();
let mockedImageManagerService = new MockedProjectService()
let mockedToasterService = new MockedToasterService()
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MatIconModule,
MatToolbarModule,
MatMenuModule,
MatCheckboxModule,
MatDialogModule,
],
providers: [
{ provide: ControllerService, useValue: mockedControllerService },
{ provide: ProjectService, useValue: mockedImageManagerService },
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} },
{ provide: ToasterService, useValue: mockedToasterService },
],
declarations: [ ConfirmationDeleteAllProjectsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmationDeleteAllProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,48 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ProjectService } from '../../../services/project.service';
import { ToasterService } from '../../../services/toaster.service';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Component({
selector: 'app-confirmation-delete-all-projects',
templateUrl: './confirmation-delete-all-projects.component.html',
styleUrls: ['./confirmation-delete-all-projects.component.scss']
})
export class ConfirmationDeleteAllProjectsComponent implements OnInit {
isDelete: boolean = false;
isUsedFiles: boolean = false;
deleteFliesDetails: any = []
fileNotDeleted: any = []
constructor(
@Inject(MAT_DIALOG_DATA) public deleteData: any,
public dialogRef: MatDialogRef<ConfirmationDeleteAllProjectsComponent>,
private projectService: ProjectService,
private toasterService: ToasterService
) { }
ngOnInit(): void {
}
async deleteAll() {
this.isDelete = true
await this.deleteFile()
}
deleteFile() {
const calls = [];
this.deleteData.deleteFilesPaths.forEach(project => {
calls.push(this.projectService.delete(this.deleteData.controller, project.project_id).pipe(catchError(error => of(error))))
});
Observable.forkJoin(calls).subscribe(responses => {
this.deleteFliesDetails = responses.filter(x => x !== null)
this.fileNotDeleted = responses.filter(x => x === null)
this.isUsedFiles = true;
this.isDelete = true
});
}
}

View File

@ -2,8 +2,6 @@
<div class="default-header">
<div class="row">
<h1 class="col">Projects</h1>
<button class="col" mat-raised-button (click)="goToSystemStatus()" class="add-button">Go to system status</button>
<button class="col" mat-raised-button (click)="goToPreferences()" class="add-button">Go to preferences</button>
<button class="col" mat-raised-button color="primary" (click)="addBlankProject()" class="add-button">
Add blank project
</button>
@ -22,6 +20,21 @@
<div class="default-content">
<div class="mat-elevation-z8">
<mat-table #table [dataSource]="dataSource | projectsfilter: searchText" matSort>
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? selectAllImages() : null" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell>
<mat-cell *matCellDef="let row">
@ -30,8 +43,8 @@
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
<mat-cell *matCellDef="let row" style="text-align: right">
<mat-header-cell *matHeaderCellDef class="action"> Actions </mat-header-cell>
<mat-cell class="action" *matCellDef="let row" style="text-align: right">
<button
mat-icon-button
matTooltip="Open project"
@ -52,20 +65,35 @@
</button>
<button
mat-icon-button
matTooltip="Duplicate project"
matTooltip="Save project as"
matTooltipClass="custom-tooltip"
(click)="duplicate(row)"
*ngIf="row.status == 'closed'"
>
<mat-icon aria-label="Duplicate project">filter_2</mat-icon>
<mat-icon aria-label="Save project as">save</mat-icon>
</button>
<button
mat-icon-button
matTooltip="Delete project"
matTooltip="Export project"
matTooltipClass="custom-tooltip"
(click)="delete(row)"
*ngIf="row.status == 'closed'"
(click)="exportSelectProject(row)"
>
<mat-icon aria-label="Export project">arrow_downward</mat-icon>
</button>
</mat-cell>
</ng-container>
<ng-container matColumnDef="delete" >
<mat-header-cell *matHeaderCellDef class="action">
<button mat-button matTooltip="Delete all projects" matTooltipClass="custom-tooltip"*ngIf="(selection.hasValue() && isAllSelected()) || selection.selected.length > 1" (click)="deleteAllFiles()" aria-label="Example icon button with a delete icon">
<mat-icon>delete</mat-icon>
</button>
</mat-header-cell>
<mat-cell *matCellDef="let row" class="action">
<button mat-icon-button matTooltip="Delete project" matTooltipClass="custom-tooltip" (click)="delete(row)" *ngIf="row.status == 'closed' && selection.isSelected(row)" aria-label="Example icon button with a delete icon" >
<mat-icon aria-label="Delete project">delete</mat-icon>
</button>
</mat-cell>

View File

@ -19,3 +19,14 @@
.row {
display: flex;
}
table {
width: 100%;
}
// mat-header-cell, mat-cell {
// justify-content: center;
// }
.action{
justify-content: center;
}

View File

@ -1,9 +1,10 @@
import { DataSource } from '@angular/cdk/collections';
import { DataSource, SelectionModel } from '@angular/cdk/collections';
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatBottomSheet } from '@angular/material/bottom-sheet';
import { MatDialog } from '@angular/material/dialog';
import { MatSort, MatSortable } from '@angular/material/sort';
import { ActivatedRoute, Router } from '@angular/router';
import { ExportPortableProjectComponent } from '../../components/export-portable-project/export-portable-project.component';
import { ElectronService } from 'ngx-electron';
import { BehaviorSubject, merge, Observable } from 'rxjs';
import { map } from 'rxjs//operators';
@ -17,6 +18,7 @@ import { ToasterService } from '../../services/toaster.service';
import { AddBlankProjectDialogComponent } from './add-blank-project-dialog/add-blank-project-dialog.component';
import { ChooseNameDialogComponent } from './choose-name-dialog/choose-name-dialog.component';
import { ConfirmationBottomSheetComponent } from './confirmation-bottomsheet/confirmation-bottomsheet.component';
import { ConfirmationDeleteAllProjectsComponent } from './confirmation-delete-all-projects/confirmation-delete-all-projects.component';
import { ImportProjectDialogComponent } from './import-project-dialog/import-project-dialog.component';
import { NavigationDialogComponent } from './navigation-dialog/navigation-dialog.component';
@ -29,10 +31,12 @@ export class ProjectsComponent implements OnInit {
controller:Controller ;
projectDatabase = new ProjectDatabase();
dataSource: ProjectDataSource;
displayedColumns = ['name', 'actions'];
displayedColumns = ['select', 'name', 'actions', 'delete'];
settings: Settings;
project: Project;
searchText: string = '';
isAllDelete: boolean = false;
selection = new SelectionModel(true, []);
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -65,18 +69,6 @@ export class ProjectsComponent implements OnInit {
this.projectService.projectListSubject.subscribe(() => this.refresh());
}
goToPreferences() {
this.router
.navigate(['/controller', this.controller.id, 'preferences'])
.catch((error) => this.toasterService.error('Cannot navigate to the preferences'));
}
goToSystemStatus() {
this.router
.navigate(['/controller', this.controller.id, 'systemstatus'])
.catch((error) => this.toasterService.error('Cannot navigate to the system status'));
}
refresh() {
this.projectService.list(this.controller).subscribe(
(projects: Project[]) => {
@ -188,6 +180,70 @@ export class ProjectsComponent implements OnInit {
}
});
}
deleteAllFiles() {
const dialogRef = this.dialog.open(ConfirmationDeleteAllProjectsComponent, {
width: '550px',
maxHeight: '650px',
autoFocus: false,
disableClose: true,
data: {
controller: this.controller,
deleteFilesPaths: this.selection.selected
}
});
dialogRef.afterClosed().subscribe((isAllfilesdeleted: boolean) => {
if (isAllfilesdeleted) {
this.unChecked()
this.refresh()
this.toasterService.success('All projects deleted');
} else {
this.unChecked()
this.refresh()
return false;
}
});
}
isAllSelected() {
const numSelected = this.selection.selected.length;
const numRows = this.projectDatabase.data.length;
return numSelected === numRows;
}
selectAllImages() {
this.isAllSelected() ? this.unChecked() : this.allChecked();
}
unChecked() {
this.selection.clear();
this.isAllDelete = false;
}
allChecked() {
this.projectDatabase.data.forEach((row) => this.selection.select(row));
this.isAllDelete = true;
}
exportSelectProject(project: Project){
this.project = project
if(this.project.project_id){
this.exportPortableProjectDialog()
}
}
exportPortableProjectDialog() {
const dialogRef = this.dialog.open(ExportPortableProjectComponent, {
width: '700px',
maxHeight: '850px',
autoFocus: false,
disableClose: true,
data: {controllerDetails:this.controller,projectDetails:this.project},
});
dialogRef.afterClosed().subscribe((isAddes: boolean) => {});
}
}
export class ProjectDatabase {

View File

@ -19,7 +19,29 @@
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item routerLink="/settings">
<button mat-menu-item
[disabled]="!controllerId"
[routerLink]="['controller', controllerId, 'systemstatus']">
<mat-icon>info</mat-icon>
<span>System status</span>
</button>
<button mat-menu-item
[disabled]="!controllerId"
[routerLink]="['controller', controllerId, 'preferences']">
<mat-icon>settings_applications</mat-icon>
<span>Template preferences</span>
</button>
<button mat-menu-item
[disabled]="!controllerId"
[routerLink]="['controller', controllerId, 'image-manager']">
<mat-icon>collections</mat-icon>
<span>Image manager</span>
</button>
<button mat-menu-item
[disabled]="!controllerId"
[routerLink]="['controller', controllerId, 'settings']"
>
<mat-icon>settings</mat-icon>
<span>Settings</span>
</button>
@ -29,7 +51,10 @@
<mat-icon>groups</mat-icon>
<span>Management</span>
</button>
<button mat-menu-item routerLink="/help">
<button mat-menu-item
[disabled]="!controllerId"
[routerLink]="['controller', controllerId, 'help']"
>
<mat-icon>help</mat-icon>
<span>Help</span>
</button>

View File

@ -73,7 +73,6 @@ export class ProjectService {
}
links(controller:Controller , project_id: string) {
debugger
return this.httpController.get<Link[]>(controller, `/projects/${project_id}/links`);
}