Description
Admin overview of all user roles and their permission rules,
linking to the details of each role.
Implements
Example
Methods
|
Async
editJson
|
editJson()
|
|
|
Edit the raw permissions config JSON as a fallback for advanced use cases.
|
|
onPageChange
|
onPageChange(event: PageEvent)
|
|
|
Parameters :
| Name |
Type |
Optional |
| event |
PageEvent
|
No
|
|
|
Readonly
addDisabledTooltip
|
Type : unknown
|
Default value : $localize`Your account does not have permission to create roles in the user account server.`
|
|
|
|
Readonly
canManageRoles
|
Type : unknown
|
Default value : this.rolePermissionsService.canManageRoles
|
|
|
whether the user may create/delete roles in the authentication server (reactive)
|
|
Readonly
defaultRole
|
Type : unknown
|
Default value : DEFAULT_ROLE
|
|
|
name and description of the "_default" role, which roles without own rules fall back to
|
|
Readonly
displayedColumns
|
Type : []
|
Default value : ["name", "description", "permissions"]
|
|
|
|
Readonly
pagedRoles
|
Type : unknown
|
Default value : computed(() => {
const start = this.pageIndex() * this.pageSize();
return this.roles().slice(start, start + this.pageSize());
})
|
|
|
|
Readonly
pageIndex
|
Type : unknown
|
Default value : signal(0)
|
|
|
|
Readonly
pageSize
|
Type : unknown
|
Default value : signal(10)
|
|
|
|
Readonly
pageSizeOptions
|
Type : []
|
Default value : [10, 25, 50, 100]
|
|
|
|
Readonly
roles
|
Type : unknown
|
Default value : signal<RoleWithPermissions[]>([])
|
|
|
import {
ChangeDetectionStrategy,
Component,
OnInit,
computed,
inject,
signal,
} from "@angular/core";
import { MatButtonModule } from "@angular/material/button";
import { MatMenuModule } from "@angular/material/menu";
import { MatPaginatorModule, PageEvent } from "@angular/material/paginator";
import { MatTableModule } from "@angular/material/table";
import { MatTooltipModule } from "@angular/material/tooltip";
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { firstValueFrom } from "rxjs";
import { ViewTitleComponent } from "../../../common-components/view-title/view-title.component";
import { Logging } from "../../../logging/logging.service";
import { JsonEditorService } from "../../json-editor/json-editor.service";
import { DEFAULT_ROLE } from "../../../permissions/reserved-roles";
import {
RolePermissionsService,
RoleWithPermissions,
} from "../role-permissions.service";
/**
* Admin overview of all user roles and their permission rules,
* linking to the details of each role.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-admin-roles-list",
imports: [
ViewTitleComponent,
MatTableModule,
MatPaginatorModule,
MatButtonModule,
MatMenuModule,
MatTooltipModule,
FaIconComponent,
RouterLink,
],
templateUrl: "./admin-roles-list.component.html",
styleUrl: "./admin-roles-list.component.scss",
})
export class AdminRolesListComponent implements OnInit {
private readonly rolePermissionsService = inject(RolePermissionsService);
private readonly jsonEditorService = inject(JsonEditorService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
readonly roles = signal<RoleWithPermissions[]>([]);
readonly pageIndex = signal(0);
readonly pageSize = signal(10);
readonly pageSizeOptions = [10, 25, 50, 100];
readonly pagedRoles = computed(() => {
const start = this.pageIndex() * this.pageSize();
return this.roles().slice(start, start + this.pageSize());
});
readonly displayedColumns = ["name", "description", "permissions"];
/** name and description of the "_default" role, which roles without own rules fall back to */
readonly defaultRole = DEFAULT_ROLE;
/** whether the user may create/delete roles in the authentication server (reactive) */
readonly canManageRoles = this.rolePermissionsService.canManageRoles;
readonly addDisabledTooltip = $localize`Your account does not have permission to create roles in the user account server.`;
ngOnInit() {
this.loadRoles();
}
private async loadRoles() {
try {
this.roles.set(await this.rolePermissionsService.loadRoles());
this.pageIndex.set(0);
} catch (err) {
Logging.error("Failed to load roles:", err);
}
}
onPageChange(event: PageEvent) {
this.pageIndex.set(event.pageIndex);
this.pageSize.set(event.pageSize);
}
openRoleDetails(role: RoleWithPermissions) {
this.router.navigate([role.name], { relativeTo: this.route });
}
/**
* Edit the raw permissions config JSON as a fallback for advanced use cases.
*/
async editJson() {
const config = await this.rolePermissionsService.loadPermissionsConfig();
const updatedData = await firstValueFrom(
this.jsonEditorService.openJsonEditorDialog(config.data),
);
if (!updatedData) return;
await this.rolePermissionsService.savePermissionsConfig(updatedData);
await this.loadRoles();
}
}
<div class="flex-row flex-wrap">
<div class="flex-grow">
<app-view-title i18n>Roles & Permissions</app-view-title>
</div>
<div class="flex-row gap-small align-center">
<!-- tooltip on the button itself, kept hoverable while disabled via
"disabledInteractive"; the null routerLink keeps the interactive
disabled button from navigating -->
<button
mat-stroked-button
color="accent"
class="standard-add-button"
[routerLink]="canManageRoles() ? 'new' : null"
[disabled]="!canManageRoles()"
[disabledInteractive]="!canManageRoles()"
[matTooltip]="addDisabledTooltip"
[matTooltipDisabled]="canManageRoles()"
[attr.aria-describedby]="
canManageRoles() ? null : 'role-add-disabled-reason'
"
>
<fa-icon icon="plus-circle" class="standard-icon-with-text"></fa-icon>
<span i18n>Add New Role</span>
</button>
@if (!canManageRoles()) {
<span class="visually-hidden" id="role-add-disabled-reason">{{
addDisabledTooltip
}}</span>
}
<button
mat-icon-button
[matMenuTriggerFor]="moreMenu"
i18n-aria-label
aria-label="More options"
>
<fa-icon icon="ellipsis-vertical"></fa-icon>
</button>
<mat-menu #moreMenu="matMenu">
<button mat-menu-item (click)="editJson()">
<fa-icon icon="wrench" class="standard-icon-with-text"></fa-icon>
<span i18n>Edit JSON</span>
</button>
</mat-menu>
</div>
</div>
<p i18n class="margin-bottom-regular">
Roles define what users can see and do in the system. Each user can have one
or several roles assigned in their user account.
</p>
<div class="table-container">
<table mat-table [dataSource]="pagedRoles()" class="full-width table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef i18n>Role</th>
<td mat-cell *matCellDef="let role">
<span class="flex-row align-center gap-small">
{{ role.name }}
@if (role.isProtected) {
<fa-icon
icon="lock"
class="protected-role-icon text-secondary"
i18n-matTooltip
matTooltip="Protected role, cannot be deleted"
></fa-icon>
}
</span>
</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef i18n>Description</th>
<td mat-cell *matCellDef="let role">{{ role.description || "-" }}</td>
</ng-container>
<ng-container matColumnDef="permissions">
<th mat-header-cell *matHeaderCellDef i18n>Permissions</th>
<td mat-cell *matCellDef="let role">
@if (role.rules) {
<span i18n>
{role.rules.length, plural,
one {1 rule}
other {{{ role.rules.length }} rules}
}
</span>
} @else if (role.isVirtual) {
<span class="text-secondary" i18n>No permissions defined</span>
} @else {
<span class="text-secondary" i18n>
No permissions defined, falls back to the "{{ defaultRole.label }}"
role
</span>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: displayedColumns"
tabindex="0"
(click)="openRoleDetails(row)"
(keydown.enter)="openRoleDetails(row)"
(keydown.space)="$event.preventDefault(); openRoleDetails(row)"
class="clickable-row"
></tr>
</table>
<mat-paginator
class="table-footer"
[length]="roles().length"
[pageIndex]="pageIndex()"
[pageSize]="pageSize()"
[pageSizeOptions]="pageSizeOptions"
[showFirstLastButtons]="true"
(page)="onPageChange($event)"
aria-label="Roles pagination"
i18n-aria-label
></mat-paginator>
</div>
@use "mixins/list-table";
// same table styling as the user accounts list
@include list-table.container-and-table;
@include list-table.footer;
@include list-table.clickable-row;
.protected-role-icon {
font-size: 11px;
opacity: 0.7;
}
Legend
Html element with directive