|
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>
@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 with directive