diff --git a/ui/src/app/affiliation/affiliations.component.ts b/ui/src/app/affiliation/affiliations.component.ts index 26a20ad1c..510699b6c 100644 --- a/ui/src/app/affiliation/affiliations.component.ts +++ b/ui/src/app/affiliation/affiliations.component.ts @@ -40,7 +40,6 @@ export class AffiliationsComponent implements OnInit, OnDestroy { importEventSubscriber: Subscription | undefined notificationSubscription: Subscription | undefined routeData: Subscription | undefined - links: any totalItems: any itemsPerPage: any page = 1 diff --git a/ui/src/app/affiliation/send-notifications-dialog.component.ts b/ui/src/app/affiliation/send-notifications-dialog.component.ts index 03c1f5ca4..068f7de96 100644 --- a/ui/src/app/affiliation/send-notifications-dialog.component.ts +++ b/ui/src/app/affiliation/send-notifications-dialog.component.ts @@ -5,11 +5,9 @@ import { EventService } from '../shared/service/event.service' import { AlertService } from '../shared/service/alert.service' import { IUser } from '../user/model/user.model' import { faPaperPlane } from '@fortawesome/free-solid-svg-icons' -import { IMember } from '../member/model/member.model' import { MemberService } from '../member/service/member.service' import { LanguageService } from '../shared/service/language.service' import { AccountService } from '../account' -import { IAccount } from '../account/model/account.model' import { AlertType, EventType } from '../app.constants' import { ActivatedRoute, Router } from '@angular/router' diff --git a/ui/src/app/app.constants.ts b/ui/src/app/app.constants.ts index c540e4e0f..924c6d581 100644 --- a/ui/src/app/app.constants.ts +++ b/ui/src/app/app.constants.ts @@ -8,6 +8,7 @@ export enum EventType { AFFILIATION_LIST_MODIFICATION, IMPORT_AFFILIATIONS, SEND_NOTIFICATIONS, + MEMBER_LIST_MODIFICATION, } export enum AlertType { diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 70605bbe8..35ed673bc 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -17,9 +17,10 @@ import { ErrorComponent } from './error/error.component' import { FormsModule } from '@angular/forms' import { UserModule } from './user/user.module' import { AffiliationModule } from './affiliation/affiliation.module' +import { MembersComponent } from './member/members.component' @NgModule({ - declarations: [AppComponent, NavbarComponent, FooterComponent, ErrorComponent], + declarations: [AppComponent, NavbarComponent, FooterComponent, ErrorComponent, MembersComponent], imports: [ BrowserModule, UserModule, diff --git a/ui/src/app/member/member.route.ts b/ui/src/app/member/member.route.ts new file mode 100644 index 000000000..a23e3ea18 --- /dev/null +++ b/ui/src/app/member/member.route.ts @@ -0,0 +1,35 @@ +import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from '@angular/router' +import { AuthGuard } from '../account/auth.guard' +import { Observable, filter, of, take } from 'rxjs' +import { inject } from '@angular/core' +import { IMember, Member } from './model/member.model' +import { MemberService } from './service/member.service' +import { MembersComponent } from './members.component' + +export const MemberResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + memberService: MemberService = inject(MemberService) +): Observable => { + if (route.paramMap.get('id')) { + return memberService.find(route.paramMap.get('id')!).pipe( + filter((member: IMember) => !!member), + take(1) + ) + } else { + return of(null) + } +} + +export const memberRoutes: Routes = [ + { + path: 'members', + component: MembersComponent, + data: { + authorities: ['ROLE_ADMIN'], + defaultSort: 'id,asc', + pageTitle: 'gatewayApp.msUserServiceMSMember.home.title.string', + }, + canActivate: [AuthGuard], + }, +] diff --git a/ui/src/app/member/members.component.html b/ui/src/app/member/members.component.html new file mode 100644 index 000000000..cab9a7566 --- /dev/null +++ b/ui/src/app/member/members.component.html @@ -0,0 +1,89 @@ +
+

Manage members

+
+
+ + +
+
+
+
+
+
+ + +
+ +
+
+
+
+
+ +
+ No members to show +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Salesforce Id Member Name Consortium Lead Parent Salesforce Id Assertions Enabled Last Modified
{{member.salesforceId}}{{member.clientName}} + {{member.isConsortiumLead}} + + + {{member.parentSalesforceId}} + {{member.assertionServiceEnabled}} + + + {{member.lastModifiedDate?.toString() | date:'medium'}} +
+ +
+
+
+
+
+

{{ itemCount }}

+
+
+ +
+
+
diff --git a/ui/src/app/member/members.component.scss b/ui/src/app/member/members.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/member/members.component.spec.ts b/ui/src/app/member/members.component.spec.ts new file mode 100644 index 000000000..98e28c9e8 --- /dev/null +++ b/ui/src/app/member/members.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MembersComponent } from './members.component'; + +describe('MembersComponent', () => { + let component: MembersComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MembersComponent] + }); + fixture = TestBed.createComponent(MembersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/member/members.component.ts b/ui/src/app/member/members.component.ts new file mode 100644 index 000000000..924ede8d1 --- /dev/null +++ b/ui/src/app/member/members.component.ts @@ -0,0 +1,176 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { IMember } from './model/member.model' +import { Subscription } from 'rxjs' +import { + faCheckCircle, + faSearch, + faSortDown, + faSortUp, + faTimes, + faTimesCircle, +} from '@fortawesome/free-solid-svg-icons' +import { MemberService } from './service/member.service' +import { ActivatedRoute, Router } from '@angular/router' +import { EventType, ITEMS_PER_PAGE } from '../app.constants' +import { AccountService } from '../account/service/account.service' +import { EventService } from '../shared/service/event.service' +import { AlertService } from '../shared/service/alert.service' +import { IMemberPage } from './model/member-page.model' + +@Component({ + selector: 'app-members', + templateUrl: './members.component.html', + styleUrls: ['./members.component.scss'], +}) +export class MembersComponent implements OnInit { + currentAccount: any + members: IMember[] | undefined | null + error: any + eventSubscriber: Subscription | undefined + routeData: any + links: any + totalItems: any + itemsPerPage: any + page: any + predicate: any + reverse: any + faTimesCircle = faTimesCircle + faCheckCircle = faCheckCircle + faTimes = faTimes + faSearch = faSearch + faSortDown = faSortDown + faSortUp = faSortUp + itemCount: string | undefined + searchTerm: string | undefined + submittedSearchTerm: string | undefined + paginationHeaderSubscription: Subscription | undefined + sortColumn = 'salesforceId' + ascending: any + + constructor( + protected memberService: MemberService, + protected alertService: AlertService, + protected accountService: AccountService, + protected activatedRoute: ActivatedRoute, + protected router: Router, + protected eventService: EventService + ) { + this.itemsPerPage = ITEMS_PER_PAGE + this.routeData = this.activatedRoute.data.subscribe((data: any) => { + this.page = data.pagingParams.page + this.reverse = data.pagingParams.ascending + this.predicate = data.pagingParams.predicate + }) + } + + ngOnInit() { + this.loadAll() + this.accountService.getAccountData().subscribe((account) => { + this.currentAccount = account + }) + + this.eventSubscriber = this.eventService.on(EventType.MEMBER_LIST_MODIFICATION).subscribe(() => { + this.searchTerm = '' + this.submittedSearchTerm = '' + this.loadAll() + }) + } + + loadAll() { + if (this.submittedSearchTerm) { + this.searchTerm = this.submittedSearchTerm + } else { + this.searchTerm = '' + } + + this.memberService + .query({ + page: this.page - 1, + size: this.itemsPerPage, + sort: this.sort(), + filter: this.submittedSearchTerm ? encodeURIComponent(this.submittedSearchTerm) : '', + }) + .subscribe({ + next: (res) => { + if (res) { + this.paginate(res) + } + }, + }) + } + + loadPage() { + this.transition() + } + + transition() { + this.router.navigate(['/members'], { + queryParams: { + page: this.page, + size: this.itemsPerPage, + sort: this.predicate + ',' + (this.reverse ? 'asc' : 'desc'), + filter: this.submittedSearchTerm ? this.submittedSearchTerm : '', + }, + }) + this.loadAll() + } + + clear() { + this.page = 0 + this.router.navigate([ + '/members', + { + page: this.page, + sort: this.predicate + ',' + (this.reverse ? 'asc' : 'desc'), + filter: this.submittedSearchTerm ? this.submittedSearchTerm : '', + }, + ]) + this.loadAll() + } + + trackId(index: number, item: IMember) { + return item.id + } + + sort() { + const result = [this.predicate + ',' + (this.reverse ? 'asc' : 'desc')] + if (this.predicate !== 'id') { + result.push('id') + } + return result + } + + resetSearch() { + this.page = 1 + this.searchTerm = '' + this.submittedSearchTerm = '' + this.loadAll() + } + + submitSearch() { + this.page = 1 + this.submittedSearchTerm = this.searchTerm + this.loadAll() + } + + protected paginate(res: IMemberPage) { + this.totalItems = res.totalItems + this.members = res.members + const first = (this.page - 1) * this.itemsPerPage === 0 ? 1 : (this.page - 1) * this.itemsPerPage + 1 + const second = this.page * this.itemsPerPage < this.totalItems ? this.page * this.itemsPerPage : this.totalItems + this.itemCount = $localize`:@@global.item-count.string:Showing ${first} - ${second} of ${this.totalItems} items.` + } + + protected onError(errorMessage: string) { + this.alertService.broadcast(errorMessage) + } + + updateSort(columnName: string) { + if (this.sortColumn && this.sortColumn == columnName) { + this.ascending = !this.ascending + } else { + this.sortColumn = columnName + } + this.loadPage() + } +} diff --git a/ui/src/app/member/model/member-page.model.ts b/ui/src/app/member/model/member-page.model.ts new file mode 100644 index 000000000..55e0681dd --- /dev/null +++ b/ui/src/app/member/model/member-page.model.ts @@ -0,0 +1,16 @@ +import { Member } from './member.model' + +export interface IMemberPage { + members: Member[] | null | undefined + totalItems: number | null | undefined +} + +export class MemberPage implements IMemberPage { + constructor( + public members: Member[], + public totalItems: number + ) { + this.members = members + this.totalItems = totalItems + } +} diff --git a/ui/src/app/member/service/member.service.ts b/ui/src/app/member/service/member.service.ts index c83654393..093aea1eb 100644 --- a/ui/src/app/member/service/member.service.ts +++ b/ui/src/app/member/service/member.service.ts @@ -3,9 +3,11 @@ import { BehaviorSubject, Observable, of, map, catchError } from 'rxjs' import { HttpClient, HttpResponse } from '@angular/common/http' import { IMember } from '../model/member.model' import * as moment from 'moment' +import { createRequestOption } from 'src/app/shared/request-util' +import { IMemberPage, MemberPage } from '../model/member-page.model' type EntityResponseType = HttpResponse -type EntityArrayResponseType = HttpResponse; +type EntityArrayResponseType = HttpResponse @Injectable({ providedIn: 'root' }) export class MemberService { @@ -14,23 +16,26 @@ export class MemberService { public resourceUrl = '/services/memberservice/api' public managedMember = new BehaviorSubject(null) - find(id: string): Observable { - return this.http - .get(`${this.resourceUrl}/members/${id}`, { - observe: 'response', + find(id: string): Observable { + return this.http.get(`${this.resourceUrl}/members/${id}`).pipe( + map((res: IMember) => this.convertDateFromServer(res)), + catchError((err) => { + return of(err) }) - .pipe( - map((res: EntityResponseType) => this.convertDateFromServer(res)), - catchError((err) => { - return of(err) - }) - ) + ) } - getAllMembers(): Observable { + getAllMembers(): Observable { return this.http - .get(`${this.resourceUrl}/members/list/all`, { observe: 'response' }) - .pipe(map((res: EntityArrayResponseType) => this.convertDateArrayFromServer(res))); + .get(`${this.resourceUrl}/members/list/all`) + .pipe(map((res: IMember[]) => this.convertMembersArrayFromServer(res))) + } + + query(req?: any): Observable { + const options = createRequestOption(req) + return this.http + .get(this.resourceUrl + 's', { params: options, observe: 'response' }) + .pipe(map((res: HttpResponse) => this.convertToMemberPage(res))) } getManagedMember(): Observable { @@ -41,21 +46,36 @@ export class MemberService { this.managedMember.next(value) } - protected convertDateFromServer(res: EntityResponseType): IMember | null { - if (res.body) { - res.body.createdDate = res.body.createdDate != null ? moment(res.body.createdDate) : undefined - res.body.lastModifiedDate = res.body.lastModifiedDate != null ? moment(res.body.lastModifiedDate) : undefined + protected convertDateFromServer(member: IMember): IMember { + if (member) { + member.createdDate = member.createdDate != null ? moment(member.createdDate) : undefined + member.lastModifiedDate = member.lastModifiedDate != null ? moment(member.lastModifiedDate) : undefined } - return res.body + return member } - protected convertDateArrayFromServer(res: EntityArrayResponseType): EntityArrayResponseType { + protected convertMembersArrayFromServer(members: IMember[]): IMember[] { + if (members) { + members.forEach((member: IMember) => { + member.createdDate = member.createdDate != null ? moment(member.createdDate) : null + member.lastModifiedDate = member.lastModifiedDate != null ? moment(member.lastModifiedDate) : null + }) + } + return members + } + + protected convertToMemberPage(res: HttpResponse): IMemberPage | null { if (res.body) { res.body.forEach((member: IMember) => { - member.createdDate = member.createdDate != null ? moment(member.createdDate) : null; - member.lastModifiedDate = member.lastModifiedDate != null ? moment(member.lastModifiedDate) : null; - }); + member.createdDate = member.createdDate ? moment(member.createdDate) : undefined + member.lastModifiedDate = member.lastModifiedDate ? moment(member.lastModifiedDate) : undefined + }) + const totalCount: string | null = res.headers.get('X-Total-Count') + if (totalCount) { + const userPage = new MemberPage(res.body, parseInt(totalCount, 10)) + return userPage + } } - return res; + return null } } diff --git a/ui/src/app/user/user-update.component.ts b/ui/src/app/user/user-update.component.ts index 32062879e..c04b29cb4 100644 --- a/ui/src/app/user/user-update.component.ts +++ b/ui/src/app/user/user-update.component.ts @@ -135,9 +135,9 @@ export class UserUpdateComponent { getMemberList(): Observable { if (this.hasRoleAdmin()) { return this.memberService.getAllMembers().pipe( - map((res) => { - if (res.body) { - return res.body + map((members) => { + if (members) { + return members } return [] })