src/app/core/dashboard/dashboard/dashboard.component.ts

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Inputs

Inputs

widgets
Type : DynamicComponentConfig[]
Default value : []

Properties

dashboardViewId
Type : string
Default value : this.activeRoute.snapshot.url.join("/")
Readonly permittedWidgets
Type : unknown
Default value : resource({ params: () => ({ widgets: this.widgets(), // include session roles so resource reloads when roles change roles: this.sessionRoles()?.roles ?? [], }), loader: async ({ params: { widgets } }) => { const roleFiltered = widgets.filter((widget) => this.hasRequiredRole(widget), ); if (!roleFiltered.length) { return []; } const widgetsWithAccess = await Promise.all( roleFiltered.map(async (widget) => { if (await this.hasEntityPermission(widget)) { return widget; } return undefined; }), ); return widgetsWithAccess.filter( (widget): widget is DynamicComponentConfig => widget !== undefined, ); }, })
import {
  Component,
  inject,
  input,
  resource,
  ChangeDetectionStrategy,
} from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface";
import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive";
import { RouteTarget } from "../../../route-target";
import { EntityAbility } from "../../permissions/ability/entity-ability";
import { ComponentRegistry } from "../../../dynamic-components";
import { SessionSubject } from "../../session/auth/session-info";
import { MatMenuModule } from "@angular/material/menu";
import { MatIconButton } from "@angular/material/button";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { ActivatedRoute, RouterLink } from "@angular/router";
import { AblePurePipe } from "@casl/angular";
import { AsyncPipe } from "@angular/common";
import { MatTooltipModule } from "@angular/material/tooltip";

@RouteTarget("Dashboard")
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-dashboard",
  templateUrl: "./dashboard.component.html",
  styleUrls: ["./dashboard.component.scss"],
  imports: [
    DynamicComponentDirective,
    MatMenuModule,
    MatIconButton,
    FaIconComponent,
    RouterLink,
    AblePurePipe,
    AsyncPipe,
    MatTooltipModule,
  ],
})
export class DashboardComponent {
  private readonly ability = inject(EntityAbility);
  private readonly components = inject(ComponentRegistry);
  private readonly session = inject(SessionSubject);
  private readonly activeRoute = inject(ActivatedRoute);

  readonly widgets = input<DynamicComponentConfig[]>([]);

  private readonly sessionRoles = toSignal(this.session, {
    initialValue: this.session.value,
  });

  readonly permittedWidgets = resource({
    params: () => ({
      widgets: this.widgets(),
      // include session roles so resource reloads when roles change
      roles: this.sessionRoles()?.roles ?? [],
    }),
    loader: async ({ params: { widgets } }) => {
      const roleFiltered = widgets.filter((widget) =>
        this.hasRequiredRole(widget),
      );
      if (!roleFiltered.length) {
        return [];
      }

      const widgetsWithAccess = await Promise.all(
        roleFiltered.map(async (widget) => {
          if (await this.hasEntityPermission(widget)) {
            return widget;
          }
          return undefined;
        }),
      );

      return widgetsWithAccess.filter(
        (widget): widget is DynamicComponentConfig => widget !== undefined,
      );
    },
  });

  dashboardViewId: string = this.activeRoute.snapshot.url.join("/");

  private hasRequiredRole(widget: DynamicComponentConfig) {
    if (widget.permittedUserRoles?.length > 0) {
      const userRoles = this.session.value.roles;
      const requiredRoles = widget.permittedUserRoles;
      return requiredRoles.some((role) => userRoles.includes(role));
    } else {
      return true;
    }
  }

  private async hasEntityPermission(widget: DynamicComponentConfig) {
    const comp = (await this.components.get(widget.component)()) as {
      getRequiredEntities?: (config: unknown) => string | string[];
    };
    let entity: string | string[] | undefined;
    if (typeof comp.getRequiredEntities === "function") {
      entity = comp.getRequiredEntities(widget.config);
    }
    return this.userHasAccess(entity);
  }

  private userHasAccess(entity: string | string[] | undefined): boolean {
    if (entity) {
      if (Array.isArray(entity)) {
        return entity.some((e) => this.ability.can("read", e));
      } else {
        return this.ability.can("read", entity);
      }
    }
    // No entity relation -> show widget
    return true;
  }
}

/**
 * Configuration DTO for dashboard view definitions (e.g. view config persistence/editing).
 * The component itself uses signal inputs and therefore does not implement this plain object shape.
 */
export interface DashboardConfig {
  widgets: DynamicComponentConfig[];
}
@if ("update" | ablePure: "Config" | async) {
  <button
    mat-icon-button
    color="primary"
    [matMenuTriggerFor]="additional"
    class="dashboard-menu-btn"
    matTooltip="Configure the dashboard"
    i18n-matTooltip
  >
    <fa-icon icon="ellipsis-v"></fa-icon>
  </button>
  <mat-menu #additional>
    <button
      mat-menu-item
      [routerLink]="['/admin/dashboard', dashboardViewId]"
      matTooltip="Configure, add and remove dashboard widgets"
      i18n-matTooltip
    >
      <fa-icon
        class="standard-icon-with-text color-accent"
        icon="tools"
      ></fa-icon>
      <span i18n>Edit Dashboard</span>
    </button>
  </mat-menu>
}

<div class="widgets-container">
  @for (widgetConfig of permittedWidgets.value() ?? []; track widgetConfig) {
    <ng-template [appDynamicComponent]="widgetConfig"></ng-template>
  }
</div>

./dashboard.component.scss

@use "mixins/grid-layout";
@use "variables/sizes";

/* The minimum size of a widget for big screens */
/* The minimum size on small screens is whatever size is available */
$min-widget-width: 330px;
/* There will only be one column below this screen width, as defined by the media query */
$max-screen-width: $min-widget-width + sizes.$margin-main-view-right +
  sizes.$margin-main-view-left;
$widget-height: 430px;

.widgets-container {
  /* Make all rows be equal in height */
  grid-auto-rows: $widget-height;

  @include grid-layout.adaptive($min-widget-width, $max-screen-width);
}

.grid-item {
  transition: all 1s;
}

.dashboard-menu-btn {
  position: fixed;
  top: 80px; // Adjust based on your header height
  right: 20px;
  z-index: 1000;
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  border-radius: 50%;

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""