Skip to content

Commit 79cce38

Browse files
feat(admin): consents for assigned members
Show consents on the page with members assigned to the resource.
1 parent 035d4d5 commit 79cce38

File tree

14 files changed

+410
-48
lines changed

14 files changed

+410
-48
lines changed

apps/admin-gui/src/app/facilities/pages/resource-detail-page/resource-assigned-members/resource-assigned-members.component.html

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
<h1 class="page-subtitle">{{'RESOURCE_DETAIL.ASSIGNED_MEMBERS.TITLE' | translate}}</h1>
22
<perun-web-apps-refresh-button (refresh)="refreshTable()"> </perun-web-apps-refresh-button>
3+
4+
<mat-form-field class="mr-2 fix-select" *ngIf="includeConsents">
5+
<mat-label>{{'RESOURCE_DETAIL.ASSIGNED_MEMBERS.FILTER_CONSENT_STATUS' | translate}}</mat-label>
6+
<mat-select (closed)="changeConsentStatuses()" [formControl]="consentStatuses" multiple>
7+
<mat-select-trigger>
8+
{{selectedConsentStatuses | selectedConsentStatuses: consentStatusList : consentStatuses.value}}
9+
</mat-select-trigger>
10+
<mat-option
11+
class="resource-theme"
12+
*ngFor="let consentStatus of consentStatusList"
13+
[value]="consentStatus">
14+
{{'CONSENTS.STATUS_' + consentStatus | translate }}
15+
</mat-option>
16+
</mat-select>
17+
</mat-form-field>
18+
319
<perun-web-apps-debounce-filter
420
(filter)="applyFilter($event)"
521
[placeholder]="'RESOURCE_DETAIL.ASSIGNED_MEMBERS.FILTER'">
@@ -9,7 +25,7 @@ <h1 class="page-subtitle">{{'RESOURCE_DETAIL.ASSIGNED_MEMBERS.TITLE' | translate
925
</ng-template>
1026
<div class="position-relative">
1127
<perun-web-apps-members-list
12-
[displayedColumns]="['id', 'fullName']"
28+
[displayedColumns]="columns"
1329
(updateTable)="refreshTable()"
1430
*perunWebAppsLoader="loading; indicator: spinner"
1531
[disableRouting]="!routeAuth"
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
11
import { Component, OnInit } from '@angular/core';
22
import { ActivatedRoute } from '@angular/router';
3-
import { Resource, ResourcesManagerService, RichMember } from '@perun-web-apps/perun/openapi';
3+
import {
4+
Consent,
5+
ConsentsManagerService,
6+
Resource,
7+
ResourcesManagerService,
8+
RichMember,
9+
} from '@perun-web-apps/perun/openapi';
410
import { TABLE_RESOURCE_MEMBERS } from '@perun-web-apps/config/table-config';
5-
import { EntityStorageService, GuiAuthResolver } from '@perun-web-apps/perun/services';
11+
import {
12+
EntityStorageService,
13+
GuiAuthResolver,
14+
StoreService,
15+
} from '@perun-web-apps/perun/services';
16+
import { MemberWithConsentStatus } from '@perun-web-apps/perun/models';
17+
import { ConsentStatusIconPipe, SelectedConsentStatusesPipe } from '@perun-web-apps/perun/pipes';
18+
import { FormControl } from '@angular/forms';
19+
import { SelectionModel } from '@angular/cdk/collections';
620

721
@Component({
822
selector: 'app-resource-assigned-members',
923
templateUrl: './resource-assigned-members.component.html',
1024
styleUrls: ['./resource-assigned-members.component.scss'],
25+
providers: [ConsentStatusIconPipe, SelectedConsentStatusesPipe],
1126
})
1227
export class ResourceAssignedMembersComponent implements OnInit {
1328
loading = false;
1429
filterValue = '';
1530
tableId = TABLE_RESOURCE_MEMBERS;
1631

1732
resource: Resource;
18-
members: RichMember[] = [];
33+
members: MemberWithConsentStatus[] = [];
34+
columns = ['id', 'fullName'];
35+
includeConsents = false;
36+
consents: Consent[] = [];
37+
consentStatuses = new FormControl();
38+
39+
consentStatusList = ['UNSIGNED', 'GRANTED', 'REVOKED'];
40+
selectedConsentStatuses: string[] = [];
41+
selection = new SelectionModel<MemberWithConsentStatus>(true, []);
1942

2043
routeAuth: boolean;
2144

2245
constructor(
2346
private route: ActivatedRoute,
2447
private resourceService: ResourcesManagerService,
2548
private authResolver: GuiAuthResolver,
26-
private entityStorageService: EntityStorageService
49+
private entityStorageService: EntityStorageService,
50+
private storeService: StoreService,
51+
private consentService: ConsentsManagerService,
52+
private consentStatusPipe: ConsentStatusIconPipe
2753
) {}
2854

2955
ngOnInit(): void {
@@ -35,21 +61,55 @@ export class ResourceAssignedMembersComponent implements OnInit {
3561
refreshTable(): void {
3662
this.loading = true;
3763
this.resourceService.getAssignedRichMembers(this.resource.id).subscribe((members) => {
38-
this.members = members;
39-
this.setAuthRights();
40-
this.loading = false;
64+
this.setAuthRights(members);
65+
this.consentService.getConsentHubByResource(this.resource.id).subscribe((hub) => {
66+
// include consents if they are turned on and enforced
67+
this.includeConsents =
68+
this.storeService.getProperty('enforce_consents') && hub.enforceConsents;
69+
if (this.includeConsents) {
70+
this.columns = ['id', 'fullName', 'consentStatus'];
71+
this.consentService
72+
.getConsentsForConsentHubByResource(this.resource.id)
73+
.subscribe((consents) => {
74+
this.consents = consents;
75+
this.members = this.getConsentsForMembers(members).filter(
76+
(member) =>
77+
!this.selectedConsentStatuses ||
78+
this.selectedConsentStatuses.length === 0 ||
79+
this.selectedConsentStatuses.includes(member.consent)
80+
);
81+
});
82+
} else {
83+
this.members = members as MemberWithConsentStatus[];
84+
}
85+
this.loading = false;
86+
});
4187
});
4288
}
4389

44-
setAuthRights(): void {
45-
if (this.members.length !== 0) {
46-
this.routeAuth = this.authResolver.isAuthorized('getMemberById_int_policy', [
47-
this.members[0],
48-
]);
90+
setAuthRights(members: RichMember[]): void {
91+
if (members.length !== 0) {
92+
this.routeAuth = this.authResolver.isAuthorized('getMemberById_int_policy', [members[0]]);
4993
}
5094
}
5195

5296
applyFilter(filterValue: string): void {
5397
this.filterValue = filterValue;
5498
}
99+
100+
getConsentsForMembers(members: RichMember[]): MemberWithConsentStatus[] {
101+
const result: MemberWithConsentStatus[] = [];
102+
members.forEach((member) => {
103+
const mwc: MemberWithConsentStatus = member;
104+
mwc.consent = this.consentStatusPipe.transform(member.userId, this.consents);
105+
result.push(mwc);
106+
});
107+
return result;
108+
}
109+
110+
changeConsentStatuses(): void {
111+
this.selection.clear();
112+
this.selectedConsentStatuses = this.consentStatuses.value as string[];
113+
this.refreshTable();
114+
}
55115
}

apps/admin-gui/src/assets/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@
253253
},
254254
"ASSIGNED_MEMBERS": {
255255
"TITLE": "Assigned members",
256-
"FILTER": "Filter members"
256+
"FILTER": "Filter members",
257+
"FILTER_CONSENT_STATUS": "Filter by consent"
257258
},
258259
"TAGS": {
259260
"TITLE": "Assigned tags",

libs/perun/components/src/lib/members-list/members-list.component.html

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div
2-
[hidden]="this.members.length === 0 || !dataSource || dataSource.filteredData.length === 0"
2+
[hidden]="!this.members || this.members.length === 0 || !dataSource || dataSource.filteredData.length === 0"
33
class="card mt-2">
44
<perun-web-apps-table-wrapper
55
(exportDisplayedData)="exportDisplayedData($event)"
@@ -134,6 +134,15 @@
134134
<th *matHeaderCellDef mat-header-cell>{{'MEMBERS_LIST.LOGINS' | translate}}</th>
135135
<td *matCellDef="let member" mat-cell>{{member | memberLogins}}</td>
136136
</ng-container>
137+
<ng-container matColumnDef="consentStatus">
138+
<th *matHeaderCellDef mat-header-cell mat-sort-header>
139+
{{'CONSENTS.STATUS' | translate}}
140+
</th>
141+
<td mat-cell *matCellDef="let member">
142+
<perun-web-apps-consent-status
143+
[consentStatus]="member.consent"></perun-web-apps-consent-status>
144+
</td>
145+
</ng-container>
137146

138147
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
139148
<tr
@@ -149,11 +158,11 @@
149158
</div>
150159

151160
<perun-web-apps-alert
152-
*ngIf="members.length !== 0 && dataSource.filteredData.length === 0"
161+
*ngIf="members && members.length !== 0 && dataSource.filteredData.length === 0"
153162
alert_type="warn">
154163
{{'SHARED_LIB.UI.ALERTS.NO_FILTER_RESULTS_ALERT' | translate}}
155164
</perun-web-apps-alert>
156165

157-
<perun-web-apps-alert *ngIf="members.length === 0" alert_type="warn">
166+
<perun-web-apps-alert *ngIf="!members || members.length === 0" alert_type="warn">
158167
{{'SHARED_LIB.UI.ALERTS.NO_MEMBERS' | translate}}
159168
</perun-web-apps-alert>

libs/perun/components/src/lib/members-list/members-list.component.ts

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { MatSort } from '@angular/material/sort';
1111
import { MatTableDataSource } from '@angular/material/table';
1212
import { MatDialog } from '@angular/material/dialog';
1313
import { SelectionModel } from '@angular/cdk/collections';
14-
import { Member, RichMember } from '@perun-web-apps/perun/openapi';
14+
import { Member } from '@perun-web-apps/perun/openapi';
1515
import {
1616
customDataSourceFilterPredicate,
1717
customDataSourceSort,
@@ -28,9 +28,14 @@ import {
2828
ChangeMemberStatusDialogComponent,
2929
MemberTreeViewDialogComponent,
3030
} from '@perun-web-apps/perun/dialogs';
31-
import { GuiAuthResolver, TableCheckbox } from '@perun-web-apps/perun/services';
31+
import {
32+
GuiAuthResolver,
33+
PerunTranslateService,
34+
TableCheckbox,
35+
} from '@perun-web-apps/perun/services';
3236
import { ActivatedRoute } from '@angular/router';
3337
import { TableWrapperComponent } from '@perun-web-apps/perun/utils';
38+
import { MemberWithConsentStatus } from '@perun-web-apps/perun/models';
3439

3540
@Component({
3641
selector: 'perun-web-apps-members-list',
@@ -40,9 +45,10 @@ import { TableWrapperComponent } from '@perun-web-apps/perun/utils';
4045
export class MembersListComponent implements OnChanges, AfterViewInit {
4146
@ViewChild(TableWrapperComponent, { static: true }) child: TableWrapperComponent;
4247
@Input() showGroupStatuses: boolean;
43-
@Input() members: RichMember[];
48+
@Input() members: MemberWithConsentStatus[] = [];
4449
@Input() searchString: string;
45-
@Input() selection: SelectionModel<RichMember> = new SelectionModel<RichMember>();
50+
@Input() selection: SelectionModel<MemberWithConsentStatus> =
51+
new SelectionModel<MemberWithConsentStatus>();
4652
@Input() displayedColumns: string[] = [
4753
'checkbox',
4854
'id',
@@ -65,7 +71,7 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
6571
@Input() filter = '';
6672
@Output() updateTable = new EventEmitter<boolean>();
6773

68-
dataSource: MatTableDataSource<RichMember>;
74+
dataSource: MatTableDataSource<MemberWithConsentStatus>;
6975
pageSizeOptions = TABLE_ITEMS_COUNT_OPTIONS;
7076
disabledRouting: boolean;
7177
groupId: number;
@@ -76,14 +82,15 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
7682
private dialog: MatDialog,
7783
private authResolver: GuiAuthResolver,
7884
private tableCheckbox: TableCheckbox,
79-
private route: ActivatedRoute
85+
private route: ActivatedRoute,
86+
private translateService: PerunTranslateService
8087
) {}
8188

8289
@ViewChild(MatSort, { static: true }) set matSort(ms: MatSort) {
8390
this.sort = ms;
8491
}
8592

86-
static getFilterDataForColumn(data: RichMember, column: string): string {
93+
static getFilterDataForColumn(data: MemberWithConsentStatus, column: string): string {
8794
switch (column) {
8895
case 'fullName':
8996
if (data.user) {
@@ -99,8 +106,8 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
99106
}
100107
}
101108

102-
static getExportDataForColumn(
103-
data: RichMember,
109+
static getSortDataForColumn(
110+
data: MemberWithConsentStatus,
104111
column: string,
105112
showGroupStatuses: boolean
106113
): string {
@@ -109,7 +116,7 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
109116
return data.id.toString();
110117
case 'fullName':
111118
if (data.user) {
112-
return parseFullName(data.user);
119+
return data.user.lastName ? data.user.lastName : data.user.firstName ?? '';
113120
}
114121
return '';
115122
case 'status':
@@ -118,15 +125,15 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
118125
return parseOrganization(data);
119126
case 'email':
120127
return parseEmail(data);
121-
case 'logins':
122-
return parseLogins(data);
128+
case 'consentStatus':
129+
return data.consent;
123130
default:
124131
return '';
125132
}
126133
}
127134

128-
static getSortDataForColumn(
129-
data: RichMember,
135+
getExportDataForColumn(
136+
data: MemberWithConsentStatus,
130137
column: string,
131138
showGroupStatuses: boolean
132139
): string {
@@ -135,7 +142,7 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
135142
return data.id.toString();
136143
case 'fullName':
137144
if (data.user) {
138-
return data.user.lastName ? data.user.lastName : data.user.firstName ?? '';
145+
return parseFullName(data.user);
139146
}
140147
return '';
141148
case 'status':
@@ -144,16 +151,20 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
144151
return parseOrganization(data);
145152
case 'email':
146153
return parseEmail(data);
154+
case 'logins':
155+
return parseLogins(data);
156+
case 'consentStatus':
157+
return this.translateService.instant(`CONSENTS.STATUS_${data.consent}`);
147158
default:
148159
return '';
149160
}
150161
}
151162

152-
getExportDataForColumnFun = (data: RichMember, column: string): string => {
153-
return MembersListComponent.getExportDataForColumn(data, column, this.showGroupStatuses);
163+
getExportDataForColumnFun = (data: MemberWithConsentStatus, column: string): string => {
164+
return this.getExportDataForColumn(data, column, this.showGroupStatuses);
154165
};
155166

156-
getSortDataForColumnFun = (data: RichMember, column: string): string => {
167+
getSortDataForColumnFun = (data: MemberWithConsentStatus, column: string): string => {
157168
return MembersListComponent.getSortDataForColumn(data, column, this.showGroupStatuses);
158169
};
159170

@@ -185,17 +196,20 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
185196

186197
setDataSource(): void {
187198
if (!this.dataSource) {
188-
this.dataSource = new MatTableDataSource<RichMember>();
199+
this.dataSource = new MatTableDataSource<MemberWithConsentStatus>();
189200
this.dataSource.sort = this.sort;
190201
this.dataSource.paginator = this.child.paginator;
191-
this.dataSource.filterPredicate = (data: RichMember, filter: string): boolean =>
202+
this.dataSource.filterPredicate = (data: MemberWithConsentStatus, filter: string): boolean =>
192203
customDataSourceFilterPredicate(
193204
data,
194205
filter,
195206
this.displayedColumns,
196207
MembersListComponent.getFilterDataForColumn
197208
);
198-
this.dataSource.sortData = (data: RichMember[], sort: MatSort): RichMember[] =>
209+
this.dataSource.sortData = (
210+
data: MemberWithConsentStatus[],
211+
sort: MatSort
212+
): MemberWithConsentStatus[] =>
199213
customDataSourceSort(data, sort, this.getSortDataForColumnFun);
200214
}
201215
this.dataSource.filter = this.filter;
@@ -218,10 +232,10 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
218232
});
219233
}
220234

221-
canBeSelected = (member: RichMember): boolean => member.membershipType === 'DIRECT';
235+
canBeSelected = (member: MemberWithConsentStatus): boolean => member.membershipType === 'DIRECT';
222236

223237
isAllSelected(): boolean {
224-
return this.tableCheckbox.isAllSelected<RichMember>(
238+
return this.tableCheckbox.isAllSelected<MemberWithConsentStatus>(
225239
this.selection.selected.length,
226240
this.dataSource,
227241
this.canBeSelected
@@ -246,8 +260,8 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
246260
// now if the status is column between displayedColumns, the "disableStatusChange" attribute is also presented
247261
// so the status change through the icon on this list isn't used anywhere. If this change should be possible
248262
// in the future, the same logic as in members-dynamic-list should be used (but for this purpose some additional
249-
// member attributes have to be presented in RichMember object - at least "isLifecycleAlterable" attribute)
250-
changeStatus(event: Event, member: RichMember): void {
263+
// member attributes have to be presented in MemberWithConsentStatus object - at least "isLifecycleAlterable" attribute)
264+
changeStatus(event: Event, member: MemberWithConsentStatus): void {
251265
event.stopPropagation();
252266
if (!this.disableStatusChange) {
253267
const config = getDefaultDialogConfig();
@@ -263,7 +277,7 @@ export class MembersListComponent implements OnChanges, AfterViewInit {
263277
}
264278
}
265279

266-
viewMemberGroupTree(member: RichMember): void {
280+
viewMemberGroupTree(member: MemberWithConsentStatus): void {
267281
const config = getDefaultDialogConfig();
268282
config.width = '800px';
269283
config.data = { member: member, groupId: this.groupId };

0 commit comments

Comments
 (0)