Files
authentik/web/src/components/notifications/NotificationDrawer.ts
T
Ken Sternberg 3842641abd web/dependency: move the notifications components into the components folder (#22241)
* ## What

         window.authentik.flow = {
             "layout": "{{ flow.layout }}",
    +        "background": "{{ flow.background }}",
    +        "title": "{{ flow.title }}",
         };

Amends the `flow.html` template and `GlobalAuthentik` parser to include new parameters, `background` and `title`, in the flow-specific part of the configuration written to the HTML `<head>` object, and to provide those parameters to client code.

## Why

The `layout` is start-up critical: it tells the Flow interface how the admin wants the Flow page to look, and allows the HTML and CSS to be pre-aligned to that condition. `layout` is determined on a per-Flow bases, not a per-Stage basis; Flows are derived from a tuple of `(Brand, Application?)`, where the opening policy *may* direct a user to a different flow if the user reached authentik via a redirect from a specific application, but will otherwise fall back to the default Flow for the Brand.

The `background` is a field that is required if the `Flow`’s layout is of type `frame_background`; in this case, the part of the viewport not dedicated to the FlowExecutor is reserved for an `<iframe>` that will be filled in with whatever the administrator specifies. Although this gives it the same priority as `layout` (whether it’s provided or undefined) for describing the [chrome](https://developer.mozilla.org/en-US/docs/Glossary/Chrome) around a challenge, it is currently not provided to the application in the start-up config; it is provided in the `challenge` and renders the IFrame as part of the initial challenge.

This patch fixes that; if `layout` is provided, `background` ought to be as well, even if it’s empty. The execution of a Challenge ought not have any influence over the look and feel of the Flow-defined appearance *around* that Challenge.

I have added `title` as well; with that, all of the current theme-and-appearance related configuration details are placed into `<head>` and can be removed from the FlowExecutor.

Server-side, `background` is currently specified: `background = FileField(blank=True, default="")` which is … interesting since we also appear to store URLs in it. I don’t see anything in the FlowSerializer that would change that from a client’s point of view.

This patch furthers the effort to separate flow execution from flow presentation.

- \[🐰\] The code has been formatted (`make web`)

* web/maint: Move notifications into the components folder; adjust imports accordingly

# What

1.  Moves the notifications folder from elements to components: the API and Notifications drawers are API-aware. If we want to separate that out and do something unique, we can, but for now, let’s just get things where they should be.

2.  Adjusts all the imports correctly.

3.  (Minor): Mutating the array and then calling `requestUpdate()`, especially when the array is then sorted-and-reversed, doesn’t save anything over creating a new array with the new item shifted onto the head, sorted once, and then saved to the property, which triggers an update automatically.
2026-05-20 17:35:07 -07:00

229 lines
8.9 KiB
TypeScript

import "#elements/EmptyState";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { isAPIResultReady } from "#common/api/responses";
import { pluckErrorDetail } from "#common/errors/network";
import { globalAK } from "#common/global";
import { actionToLabel, severityToLevel } from "#common/labels";
import { formatElapsedTime } from "#common/temporal";
import { AKElement } from "#elements/Base";
import { WithNotifications } from "#elements/mixins/notifications";
import { WithSession } from "#elements/mixins/session";
import { SlottedTemplateResult } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { AKDrawerChangeEvent } from "#components/notifications/events";
import { Notification } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
import { repeat } from "lit/directives/repeat.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFNotificationDrawer from "@patternfly/patternfly/components/NotificationDrawer/notification-drawer.css";
@customElement("ak-notification-drawer")
export class NotificationDrawer extends WithNotifications(WithSession(AKElement)) {
static styles: CSSResult[] = [
PFButton,
PFNotificationDrawer,
PFContent,
PFDropdown,
css`
.pf-c-drawer__body {
height: 100%;
}
.pf-c-notification-drawer__body {
flex-grow: 1;
overflow-x: hidden;
}
.pf-c-notification-drawer__header {
align-items: center;
}
.pf-c-notification-drawer__header-action,
.pf-c-notification-drawer__header-action-close,
.pf-c-notification-drawer__header-action-close > .pf-c-button.pf-m-plain {
height: 100%;
}
.pf-c-notification-drawer__list-item-description {
white-space: pre-wrap;
}
.pf-c-notification-drawer__list-item-action {
display: flex;
flex-flow: row;
align-items: start;
gap: var(--pf-global--spacer--sm);
}
`,
];
#APIBase = globalAK().api.base;
//#region Rendering
protected renderHyperlink(item: Notification) {
if (!item.hyperlink) {
return nothing;
}
return html`<small><a href=${item.hyperlink}>${item.hyperlinkLabel}</a></small>`;
}
#renderItem = (item: Notification): TemplateResult => {
const label = actionToLabel(item.event?.action);
const level = severityToLevel(item.severity);
// There's little information we can have to determine if the body
// contains code, but if it looks like JSON, we can at least style it better.
const code = item.body.includes("{");
return html`<li
class="pf-c-notification-drawer__list-item"
data-notification-action=${ifPresent(item.event?.action)}
>
<div class="pf-c-notification-drawer__list-item-header">
<span class="pf-c-notification-drawer__list-item-header-icon ${level}">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</span>
<h2 class="pf-c-notification-drawer__list-item-header-title">${label}</h2>
</div>
<div class="pf-c-notification-drawer__list-item-action">
${item.event &&
html`
<a
class="pf-c-dropdown__toggle pf-m-plain"
href="${this.#APIBase}if/admin/#/events/log/${item.event?.pk}"
aria-label=${msg(str`View details for ${label}`)}
>
<pf-tooltip position="top" content=${msg("Show details")}>
<i class="fas fa-share-square" aria-hidden="true"></i>
</pf-tooltip>
</a>
`}
<button
class="pf-c-dropdown__toggle pf-m-plain"
type="button"
@click=${() => this.markAsRead(item.pk)}
aria-label=${msg("Mark as read")}
>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
${code && item.event?.context
? html`<pre class="pf-c-notification-drawer__list-item-description">
${JSON.stringify(item.event.context, null, 2)}</pre
>`
: html`<p class="pf-c-notification-drawer__list-item-description">${item.body}</p>`}
<small class="pf-c-notification-drawer__list-item-timestamp"
><pf-tooltip position="top" .content=${item.created?.toLocaleString()}>
${formatElapsedTime(item.created!)}
</pf-tooltip></small
>
${this.renderHyperlink(item)}
</li>`;
};
protected renderEmpty() {
return html`<ak-empty-state
><span>${msg("No notifications found.")}</span>
<div slot="body">${msg("You don't have any notifications currently.")}</div>
</ak-empty-state>`;
}
protected renderBody() {
return guard([this.notifications], () => {
if (this.notifications.loading) {
return html`<ak-empty-state default-label></ak-empty-state>`;
}
if (this.notifications.error) {
return html`<ak-empty-state icon="fa-ban"
><span>${msg("Failed to fetch notifications.")}</span>
<div slot="body">${pluckErrorDetail(this.notifications.error)}</div>
</ak-empty-state>`;
}
if (!this.notificationCount) {
return this.renderEmpty();
}
return html`<ul class="pf-c-notification-drawer__list" role="list">
${repeat(
this.notifications.results,
(n) => n.pk,
(n) => this.#renderItem(n),
)}
</ul>`;
});
}
protected override render(): SlottedTemplateResult {
const unreadCount = isAPIResultReady(this.notifications) ? this.notificationCount : 0;
return html`<aside
class="pf-c-drawer__body pf-m-no-padding"
aria-labelledby="notification-drawer-title"
>
<div class="pf-c-notification-drawer">
<header class="pf-c-notification-drawer__header">
<div class="text">
<h2
id="notification-drawer-title"
class="pf-c-notification-drawer__header-title"
>
${msg("Notifications")}
</h2>
<span aria-live="polite" aria-atomic="true">
${msg(str`${unreadCount} unread`, {
id: "notification-unread-count",
desc: "Indicates the number of unread notifications in the notification drawer",
})}
</span>
</div>
<div class="pf-c-notification-drawer__header-action">
<button
@click=${this.clearNotifications}
class="pf-c-button pf-m-plain"
type="button"
aria-label=${msg("Clear all notifications", {
id: "notification-drawer-clear-all",
})}
?disabled=${!unreadCount}
>
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
<button
@click=${AKDrawerChangeEvent.dispatchNotificationsToggle}
class="pf-c-button pf-m-plain"
type="button"
aria-label=${msg("Close notification drawer", {
id: "notification-drawer-close",
})}
>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
</header>
<div class="pf-c-notification-drawer__body">${this.renderBody()}</div>
</div>
</aside>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-notification-drawer": NotificationDrawer;
}
}