src/app/core/admin/admin-role-permissions/roles-list/admin-roles-list.component.ts

Description

Admin overview of all user roles and their permission rules, linking to the details of each role.

Implements

OnInit

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Methods

Async editJson
editJson()

Edit the raw permissions config JSON as a fallback for advanced use cases.

Returns : any
onPageChange
onPageChange(event: PageEvent)
Parameters :
Name Type Optional
event PageEvent No
Returns : void
openRoleDetails
openRoleDetails(role: RoleWithPermissions)
Parameters :
Name Type Optional
role RoleWithPermissions No
Returns : void

Properties

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>

./admin-roles-list.component.scss

@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
Component
Html element with directive

results matching ""

    No results matching ""