Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User import dialog component #1122

Merged
merged 7 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions ui/src/app/shared/service/file-upload.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core'
import { HttpClient, HttpRequest, HttpEvent, HttpResponse } from '@angular/common/http'
import { Observable } from 'rxjs/internal/Observable'
import { filter, map, of } from 'rxjs'

@Injectable({ providedIn: 'root' })
export class FileUploadService {
constructor(private http: HttpClient) {}

uploadFile(
resourceUrl: string,
file: File,
expectedResponseType: 'arraybuffer' | 'blob' | 'json' | 'text' | undefined
): Observable<string> {
console.log('uploading file')
const formdata: FormData = new FormData()
formdata.append('file', file)
const req = new HttpRequest('POST', resourceUrl, formdata, {
reportProgress: true,
responseType: expectedResponseType,
})

return this.http.request<string>(req).pipe(
filter((event: HttpEvent<string>): event is HttpResponse<string> => event instanceof HttpResponse),
map((res: HttpResponse<string>) => {
return res.body != null ? res.body : ''
})
)
}
}
44 changes: 44 additions & 0 deletions ui/src/app/user/user-import-dialog.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<form name="uploadForm" (ngSubmit)="upload()" >
<div class="modal-header">
<h4 class="modal-title" i18n="@@gatewayApp.msUserServiceMSUser.import.title.string">Import users</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"
(click)="clear()">&times;</button>
</div>
<div class="modal-body">
<app-error-alert></app-error-alert>
<div class="alerts top right" role="alert" *ngIf="csvErrors && csvErrors.length > 0">
<ngb-alert class="alert alert-danger alert-dismissible" role="alert" ng-reflect-type="danger" (close)="csvErrors = null">
<p i18n="@@gatewayApp.msUserServiceMSUser.import.errors.label.string">Oops! There was a problem processing your data. Pleases fix the errors below and try again</p>
<table>
<thead>
<th>Errors</th>
</thead>
<tbody>
<tr *ngFor="let error of csvErrors">
<td>Row {{error['index']}}</td>
<td>{{error['message']}}</td>
</tr>
</tbody>
</table>
</ngb-alert>
</div>
<p id="jhi-delete-msUser-heading" i18n="@@gatewayApp.msUserServiceMSUser.import.label.string">Please select a CSV file to upload</p>

<div class="form-group">
<label class="form-control-label sr-only" i18n="@@gatewayApp.msUserServiceMSUser.import.filePath.string" for="field_filePath.string">File Path</label>
<input type="file" class="form-control" name="filePath" id="field_filePath" accept=".csv"
onclick="this.value=null" (change)="selectFile($event)"/>
</div>
</div>
<div *ngIf="loading" class="progress progress-striped">
<div class="progress-bar indeterminate" role="progressbar"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" data-dismiss="modal" (click)="clear()">
<fa-icon [icon]="faBan"></fa-icon>&nbsp;<span i18n="@@entity.action.cancel.string">Cancel</span>
</button>
<button id="jhi-confirm-delete-msUser" type="submit" class="btn btn-primary" >
<fa-icon [icon]="faSave"></fa-icon>&nbsp;<span i18n="@@entity.action.upload.string">Upload</span>
</button>
</div>
</form>
Empty file.
88 changes: 88 additions & 0 deletions ui/src/app/user/user-import-dialog.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'

import { UserImportDialogComponent } from './user-import-dialog.component'
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
import { RouterTestingModule } from '@angular/router/testing'
import { UserService } from './service/user.service'
import { ErrorService } from '../error/service/error.service'
import { EventService } from '../shared/service/event.service'
import { FileUploadService } from '../shared/service/file-upload.service'
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EMPTY, of } from 'rxjs'

describe('UserImportDialogComponent', () => {
let component: UserImportDialogComponent
let fixture: ComponentFixture<UserImportDialogComponent>

let userService: jasmine.SpyObj<UserService>
let eventService: jasmine.SpyObj<EventService>
let uploadService: jasmine.SpyObj<FileUploadService>

beforeEach(() => {
const userServiceSpy = jasmine.createSpyObj('UserService', [
'validate',
'update',
'sendActivate',
'hasOwner',
'create',
'update',
])
const eventServiceSpy = jasmine.createSpyObj('EventService', ['broadcast', 'on'])
const uploadServiceSpy = jasmine.createSpyObj('FileUploadService', ['uploadFile'])

TestBed.configureTestingModule({
declarations: [UserImportDialogComponent],
imports: [HttpClientTestingModule],
providers: [
FormBuilder,
NgbModal,
NgbActiveModal,
{ provide: UserService, useValue: userServiceSpy },
{ provide: EventService, useValue: eventServiceSpy },
{ provide: FileUploadService, useValue: uploadServiceSpy },
{ provide: ErrorService, useValue: {} },
],
}).compileComponents()

fixture = TestBed.createComponent(UserImportDialogComponent)
component = fixture.componentInstance

userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>
eventService = TestBed.inject(EventService) as jasmine.SpyObj<EventService>
uploadService = TestBed.inject(FileUploadService) as jasmine.SpyObj<FileUploadService>
})

it('should create', () => {
expect(component).toBeTruthy()
})

it('should call upload service', () => {
component.currentFile = getFileList()
uploadService.uploadFile.and.returnValue(EMPTY)
component.upload()
expect(uploadService.uploadFile).toHaveBeenCalled()
})

it('errors should be parsed', () => {
component.currentFile = getFileList()
uploadService.uploadFile.and.returnValue(
of('[{"index":1,"message":"A user with email [email protected] already exists"}]')
)
component.upload()
expect(uploadService.uploadFile).toHaveBeenCalled()
expect(component.csvErrors.length).toEqual(1)
})

const getFileList = () => {
const blob = new Blob([''], { type: 'text/html' })
const file = <File>blob
const fileList: FileList = {
0: file,
1: file,
length: 2,
item: (index: number) => file,
}
return fileList
}
})
109 changes: 109 additions & 0 deletions ui/src/app/user/user-import-dialog.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { FileUploadService } from '../shared/service/file-upload.service'
import { IUser } from './model/user.model'
import { UserService } from './service/user.service'
import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { EventService } from '../shared/service/event.service'
import { Event } from '../shared/model/event.model'
import { EventType } from '../app.constants'
import { ActivatedRoute, Router } from '@angular/router'
import { faSave, faBan, faLaptopHouse } from '@fortawesome/free-solid-svg-icons'

@Component({
selector: 'app-user-import-dialog',
templateUrl: './user-import-dialog.component.html',
styleUrls: ['./user-import-dialog.component.scss'],
})
export class UserImportDialogComponent {
resourceUrl: string
user: IUser | null
isSaving: boolean
currentFile: FileList | null
csvErrors: any
loading = false
faBan = faBan
faSave = faSave

constructor(
protected userService: UserService,
public activeModal: NgbActiveModal,
protected eventService: EventService,
private fileUploadService: FileUploadService
) {
this.isSaving = false
this.user = null
this.currentFile = null
this.resourceUrl = this.userService.resourceUrl + '/upload'
}

clear() {
this.activeModal.dismiss('cancel')
}

selectFile(event: any) {
this.currentFile = event.target.files
}

upload() {
if (this.currentFile) {
this.loading = true
const f = this.currentFile.item(0)

this.fileUploadService.uploadFile(this.resourceUrl, f!, 'text').subscribe((res: string) => {
if (res) {
this.csvErrors = JSON.parse(res)
this.loading = false
if (this.csvErrors.length === 0) {
this.eventService.broadcast(new Event(EventType.USER_LIST_MODIFIED, 'New user settings uploaded'))
this.activeModal.dismiss(true)
}
}
})
} else {
alert(
$localize`:gatewayApp.msUserServiceMSUser.import.emptyFile.string:There is no file to upload. Please select one.`
)
}
}
}

@Component({
selector: 'app-user-import-popup',
template: '',
})
export class UserImportPopupComponent implements OnInit, OnDestroy {
protected ngbModalRef: NgbModalRef | undefined | null

constructor(
protected activatedRoute: ActivatedRoute,
protected router: Router,
protected modalService: NgbModal
) {}

ngOnInit() {
this.activatedRoute.data.subscribe(({ u }) => {
setTimeout(() => {
this.ngbModalRef = this.modalService.open(UserImportDialogComponent as Component, {
size: 'lg',
backdrop: 'static',
})
this.ngbModalRef.componentInstance.user = u
this.ngbModalRef.result.then(
(result) => {
this.router.navigate(['/users', { outlets: { popup: null } }])
this.ngbModalRef = null
},
(reason) => {
this.router.navigate(['/users', { outlets: { popup: null } }])
this.ngbModalRef = null
}
)
}, 0)
})
}

ngOnDestroy() {
this.ngbModalRef = null
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => this.router.navigate(['/users']))
}
}
3 changes: 3 additions & 0 deletions ui/src/app/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { UserUpdateComponent } from './user-update.component'
import { UserDetailComponent } from './user-detail.component'
import { UserImportDialogComponent, UserImportPopupComponent } from './user-import-dialog.component'
import { UserDeleteDialogComponent, UserDeletePopupComponent } from './user-delete.component'

@NgModule({
Expand All @@ -25,6 +26,8 @@ import { UserDeleteDialogComponent, UserDeletePopupComponent } from './user-dele
UserDetailComponent,
UserDeletePopupComponent,
UserDeleteDialogComponent,
UserImportDialogComponent,
UserImportPopupComponent,
],
})
export class UserModule {}
14 changes: 14 additions & 0 deletions ui/src/app/user/user.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { User } from './model/user.model'
import { UserService } from './service/user.service'
import { Injectable, inject } from '@angular/core'
import { UserUpdateComponent } from './user-update.component'
import { UserImportPopupComponent } from './user-import-dialog.component'
import { UserDeletePopupComponent } from './user-delete.component'

export const UserResolver: ResolveFn<User | null> = (
Expand Down Expand Up @@ -49,6 +50,19 @@ export const routes: Routes = [
canActivate: [AuthGuard],
outlet: 'popup',
},
{
path: 'import',
component: UserImportPopupComponent,
resolve: {
user: UserResolver,
},
data: {
authorities: ['ROLE_ADMIN'],
pageTitle: 'gatewayApp.msUserServiceMSUser.home.title.string',
},
canActivate: [AuthGuard],
outlet: 'popup',
},
],
},
{
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/user/users.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ <h1 id="page-heading" class="mt-3" i18n="@@gatewayApp.msUserServiceMSUser.home.t
[routerLink]="['/users/new']"
>
<fa-icon [icon]="faPlus"></fa-icon>
<span i18n="@@gatewayApp.msUserServiceMSUser.home.createLabel.string"> Add user </span>
<span i18n="@@gatewayApp.msUserServiceMSUser.home.createLabel.string">Add user </span>
</button>
<button
*appHasAnyAuthority="'ROLE_ADMIN'"
id="jh-upload-entities"
class="btn btn-primary float-right jh-create-entity create-ms-user ml-1"
[routerLink]="['users', { outlets: { popup: 'import' } }]"
[routerLink]="['/' + 'users', { outlets: { popup: 'import' } }]"
>
<fa-icon [icon]="faPlus"></fa-icon>
<span i18n="@@gatewayApp.msUserServiceMSUser.home.uploadLabel.string"> Import users from CSV </span>
<span i18n="@@gatewayApp.msUserServiceMSUser.home.uploadLabel.string">Import users from CSV </span>
</button>
</div>
</div>
Expand Down
Loading