web/admin: Refine navigation (#12441)

* fix spacing if there's no icon in page header

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add a very slight bar

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework navigation to be similar between interfaces

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix subpath and rendering

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix display

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add version to sidebar

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make page header sticky?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* unrelated: hide session in system api

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* unrelated: add unidecode for policies

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#5859

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2024-12-21 22:12:47 +01:00
committed by GitHub
parent 5fccbd7c04
commit 02bd699917
13 changed files with 382 additions and 284 deletions

View File

@ -7,7 +7,9 @@ from sys import version as python_version
from typing import TypedDict from typing import TypedDict
from cryptography.hazmat.backends.openssl.backend import backend from cryptography.hazmat.backends.openssl.backend import backend
from django.conf import settings
from django.utils.timezone import now from django.utils.timezone import now
from django.views.debug import SafeExceptionReporterFilter
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
@ -52,10 +54,16 @@ class SystemInfoSerializer(PassiveSerializer):
def get_http_headers(self, request: Request) -> dict[str, str]: def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers""" """Get HTTP Request headers"""
headers = {} headers = {}
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
for key, value in request.META.items(): for key, value in request.META.items():
if not isinstance(value, str): if not isinstance(value, str):
continue continue
headers[key] = value actual_value = value
if raw_session in actual_value:
actual_value = actual_value.replace(
raw_session, SafeExceptionReporterFilter.cleansed_substitute
)
headers[key] = actual_value
return headers return headers
def get_http_host(self, request: Request) -> str: def get_http_host(self, request: Request) -> str:

13
poetry.lock generated
View File

@ -5225,6 +5225,17 @@ files = [
[package.dependencies] [package.dependencies]
ua-parser = "*" ua-parser = "*"
[[package]]
name = "unidecode"
version = "1.3.8"
description = "ASCII transliterations of Unicode text"
optional = false
python-versions = ">=3.5"
files = [
{file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"},
{file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"},
]
[[package]] [[package]]
name = "uritemplate" name = "uritemplate"
version = "4.1.1" version = "4.1.1"
@ -5934,4 +5945,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "~3.12" python-versions = "~3.12"
content-hash = "38089ad25be7638c118f4b503ad2f8495c941667f5485efe60b2bbdb14d6f44c" content-hash = "527289cd983966592bc04f308917604dc5d4a4116d8071d5e3a0bf63d216a086"

View File

@ -143,6 +143,7 @@ swagger-spec-validator = "*"
tenant-schemas-celery = "*" tenant-schemas-celery = "*"
twilio = "*" twilio = "*"
ua-parser = "*" ua-parser = "*"
unidecode = "*"
# Pinned because of botocore https://github.com/orgs/python-poetry/discussions/7937 # Pinned because of botocore https://github.com/orgs/python-poetry/discussions/7937
urllib3 = { extras = ["secure"], version = "<3" } urllib3 = { extras = ["secure"], version = "<3" }
uvicorn = { extras = ["standard"], version = "*" } uvicorn = { extras = ["standard"], version = "*" }

View File

@ -96,7 +96,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
render(): TemplateResult { render(): TemplateResult {
const name = this.user?.user.name ?? this.user?.user.username; const name = this.user?.user.name ?? this.user?.user.username;
return html`<ak-page-header icon="" header="" description=${msg("General system status")}> return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span> <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
</ak-page-header> </ak-page-header>
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">

View File

@ -3,6 +3,7 @@
--ak-global--Color--100: var(--ak-dark-foreground) !important; --ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker); --pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--link--Color: var(--ak-dark-foreground-link) !important; --pf-global--link--Color: var(--ak-dark-foreground-link) !important;
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
} }
body { body {
background-color: var(--ak-dark-background) !important; background-color: var(--ak-dark-background) !important;
@ -31,9 +32,6 @@ body {
.notification-trigger { .notification-trigger {
background-color: transparent !important; background-color: transparent !important;
} }
.pf-c-page__main-section.pf-m-light {
background-color: transparent;
}
.pf-c-content { .pf-c-content {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }

View File

@ -7,6 +7,7 @@ export enum UserDisplay {
username = "username", username = "username",
name = "name", name = "name",
email = "email", email = "email",
none = "none",
} }
export enum LayoutType { export enum LayoutType {

View File

@ -0,0 +1,210 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import {
EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFBrand from "@patternfly/patternfly/components/Brand/brand.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
@customElement("ak-nav-buttons")
export class NavigationButtons extends AKElement {
@property({ type: Object })
uiConfig?: UIConfig;
@property({ type: Object })
me?: SessionUser;
@property({ type: Boolean, reflect: true })
notificationDrawerOpen = false;
@property({ type: Boolean, reflect: true })
apiDrawerOpen = false;
@property({ type: Number })
notificationsCount = 0;
static get styles() {
return [
PFBase,
PFDisplay,
PFBrand,
PFPage,
PFAvatar,
PFButton,
PFDrawer,
PFDropdown,
PFNotificationBadge,
css`
.pf-c-page__header-tools {
display: flex;
}
`,
];
}
async firstUpdated() {
this.me = await me();
const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false,
ordering: "-created",
pageSize: 1,
user: this.me.user.pk,
});
this.notificationsCount = notifications.pagination.count;
this.uiConfig = await uiConfig();
}
renderApiDrawerTrigger() {
if (!this.uiConfig?.enabledFeatures.apiDrawer) {
return nothing;
}
const onClick = (ev: Event) => {
ev.stopPropagation();
this.dispatchEvent(
new Event(EVENT_API_DRAWER_TOGGLE, { bubbles: true, composed: true }),
);
};
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
<button class="pf-c-button pf-m-plain" type="button" @click=${onClick}>
<pf-tooltip position="top" content=${msg("Open API drawer")}>
<i class="fas fa-code" aria-hidden="true"></i>
</pf-tooltip>
</button>
</div>`;
}
renderNotificationDrawerTrigger() {
if (!this.uiConfig?.enabledFeatures.notificationDrawer) {
return nothing;
}
const onClick = (ev: Event) => {
ev.stopPropagation();
this.dispatchEvent(
new Event(EVENT_NOTIFICATION_DRAWER_TOGGLE, { bubbles: true, composed: true }),
);
};
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
<button
class="pf-c-button pf-m-plain"
type="button"
aria-label="${msg("Unread notifications")}"
@click=${onClick}
>
<span
class="pf-c-notification-badge ${this.notificationsCount > 0
? "pf-m-unread"
: ""}"
>
<pf-tooltip position="top" content=${msg("Open Notification drawer")}>
<i class="fas fa-bell" aria-hidden="true"></i>
</pf-tooltip>
<span class="pf-c-notification-badge__count">${this.notificationsCount}</span>
</span>
</button>
</div> `;
}
renderSettings() {
if (!this.uiConfig?.enabledFeatures.settings) {
return nothing;
}
return html`<div class="pf-c-page__header-tools-item">
<a class="pf-c-button pf-m-plain" type="button" href="#/settings">
<pf-tooltip position="top" content=${msg("Settings")}>
<i class="fas fa-cog" aria-hidden="true"></i>
</pf-tooltip>
</a>
</div>`;
}
renderImpersonation() {
if (!this.me?.original) {
return nothing;
}
const onClick = () => {
return new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
};
return html`&nbsp;
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
<ak-action-button class="pf-m-warning pf-m-small" .apiRequest=${onClick}>
${msg("Stop impersonation")}
</ak-action-button>
</div>
</div>`;
}
get userDisplayName() {
return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay)
.with(UserDisplay.username, () => this.me?.user.username)
.with(UserDisplay.name, () => this.me?.user.name)
.with(UserDisplay.email, () => this.me?.user.email || "")
.with(UserDisplay.none, () => "")
.otherwise(() => this.me?.user.username);
}
render() {
return html`<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
${this.renderApiDrawerTrigger()}
<!-- -->
${this.renderNotificationDrawerTrigger()}
<!-- -->
${this.renderSettings()}
<div class="pf-c-page__header-tools-item">
<a
href="${globalAK().api.base}flows/-/default/invalidation/"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Sign out")}>
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</pf-tooltip>
</a>
</div>
<slot name="extra"></slot>
</div>
${this.renderImpersonation()}
${this.userDisplayName != ""
? html`<div class="pf-c-page__header-tools-group">
<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md">
${this.userDisplayName}
</div>
</div>`
: nothing}
<img
class="pf-c-avatar"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>
</div>`;
}
}

View File

@ -1,27 +1,30 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { import {
EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE, EVENT_SIDEBAR_TOGGLE,
EVENT_WS_MESSAGE, EVENT_WS_MESSAGE,
TITLE_DEFAULT, TITLE_DEFAULT,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { currentInterface } from "@goauthentik/common/sentry"; import { currentInterface } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventsApi } from "@goauthentik/api"; import { EventsApi, SessionUser } from "@goauthentik/api";
@customElement("ak-page-header") @customElement("ak-page-header")
export class PageHeader extends WithBrandConfig(AKElement) { export class PageHeader extends WithBrandConfig(AKElement) {
@ -31,8 +34,8 @@ export class PageHeader extends WithBrandConfig(AKElement) {
@property({ type: Boolean }) @property({ type: Boolean })
iconImage = false; iconImage = false;
@property({ type: Boolean }) @state()
hasNotifications = false; notificationsCount = 0;
@property() @property()
header = ""; header = "";
@ -40,21 +43,39 @@ export class PageHeader extends WithBrandConfig(AKElement) {
@property() @property()
description?: string; description?: string;
@property({ type: Boolean })
hasIcon = true;
@state()
me?: SessionUser;
@state()
uiConfig!: UIConfig;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
PFButton, PFButton,
PFPage, PFPage,
PFNotificationBadge,
PFContent, PFContent,
PFAvatar,
PFDropdown,
css` css`
:host {
position: sticky;
top: 0;
z-index: 100;
}
.bar { .bar {
border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-height: 114px; min-height: 114px;
} max-height: 114px;
.pf-c-button.pf-m-plain { background-color: var(--pf-c-page--BackgroundColor);
background-color: transparent;
border-radius: 0px;
} }
.pf-c-page__main-section.pf-m-light { .pf-c-page__main-section.pf-m-light {
background-color: transparent; background-color: transparent;
@ -81,6 +102,15 @@ export class PageHeader extends WithBrandConfig(AKElement) {
flex-direction: row; flex-direction: row;
align-items: center !important; align-items: center !important;
} }
.pf-c-page__header-tools {
flex-shrink: 0;
}
.pf-c-page__header-tools-group {
height: 100%;
}
:host([theme="dark"]) .pf-c-page__header-tools {
color: var(--ak-dark-foreground) !important;
}
`, `,
]; ];
} }
@ -92,19 +122,17 @@ export class PageHeader extends WithBrandConfig(AKElement) {
}); });
} }
firstUpdated(): void { async firstUpdated() {
me().then((user) => { this.me = await me();
new EventsApi(DEFAULT_CONFIG) this.uiConfig = await uiConfig();
.eventsNotificationsList({ this.uiConfig.navbar.userDisplay = UserDisplay.none;
seen: false, const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
ordering: "-created", seen: false,
pageSize: 1, ordering: "-created",
user: user.user.pk, pageSize: 1,
}) user: this.me.user.pk,
.then((r) => {
this.hasNotifications = r.pagination.count > 0;
});
}); });
this.notificationsCount = notifications.pagination.count;
} }
setTitle(header?: string) { setTitle(header?: string) {
@ -126,7 +154,7 @@ export class PageHeader extends WithBrandConfig(AKElement) {
this.setTitle(this.header); this.setTitle(this.header);
} }
renderIcon(): TemplateResult { renderIcon() {
if (this.icon) { if (this.icon) {
if (this.iconImage && !this.icon.startsWith("fa://")) { if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`; return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
@ -134,7 +162,7 @@ export class PageHeader extends WithBrandConfig(AKElement) {
const icon = this.icon.replaceAll("fa://", "fa "); const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>`; return html`<i class=${icon}></i>`;
} }
return html``; return nothing;
} }
render(): TemplateResult { render(): TemplateResult {
@ -155,44 +183,19 @@ export class PageHeader extends WithBrandConfig(AKElement) {
<section class="pf-c-page__main-section pf-m-light"> <section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content"> <div class="pf-c-content">
<h1> <h1>
<slot name="icon">${this.renderIcon()}</slot>&nbsp; ${this.hasIcon
? html`<slot name="icon">${this.renderIcon()}</slot>&nbsp;`
: nothing}
<slot name="header">${this.header}</slot> <slot name="header">${this.header}</slot>
</h1> </h1>
${this.description ? html`<p>${this.description}</p>` : html``} ${this.description ? html`<p>${this.description}</p>` : html``}
</div> </div>
</section> </section>
<button <div class="pf-c-page__header-tools">
class="notification-trigger pf-c-button pf-m-plain" <div class="pf-c-page__header-tools-group">
@click=${() => { <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}></ak-nav-buttons>
this.dispatchEvent( </div>
new CustomEvent(EVENT_API_DRAWER_TOGGLE, { </div>
bubbles: true,
composed: true,
}),
);
}}
>
<pf-tooltip position="top" content=${msg("Open API drawer")}>
<i class="fas fa-code"></i>
</pf-tooltip>
</button>
<button
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications
? "has-notifications"
: ""}"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_NOTIFICATION_DRAWER_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<pf-tooltip position="top" content=${msg("Open Notification drawer")}>
<i class="fas fa-bell"></i>
</pf-tooltip>
</button>
</div>`; </div>`;
} }
} }

View File

@ -1,6 +1,6 @@
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/sidebar/SidebarBrand"; import "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarUser"; import "@goauthentik/elements/sidebar/SidebarVersion";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
@ -74,7 +74,7 @@ export class Sidebar extends AKElement {
<ul class="pf-c-nav__list"> <ul class="pf-c-nav__list">
<slot></slot> <slot></slot>
</ul> </ul>
<ak-sidebar-user></ak-sidebar-user> <ak-sidebar-version></ak-sidebar-version>
</nav>`; </nav>`;
} }
} }

View File

@ -42,6 +42,9 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
align-items: center; align-items: center;
height: 114px; height: 114px;
min-height: 114px; min-height: 114px;
border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100);
} }
.pf-c-brand img { .pf-c-brand img {
padding: 0 0.5rem; padding: 0 0.5rem;

View File

@ -1,70 +0,0 @@
import { globalAK } from "@goauthentik/common/global";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-sidebar-user")
export class SidebarUser extends AKElement {
static get styles(): CSSResult[] {
return [
PFBase,
PFNav,
PFAvatar,
css`
:host {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
}
.pf-c-nav__link {
align-items: center;
display: flex;
justify-content: center;
}
`,
];
}
render(): TemplateResult {
return html`
<a
href="${globalAK().api.base}if/user/#/settings"
class="pf-c-nav__link user-avatar"
id="user-settings"
>
${until(
me().then((u) => {
return html`<img
class="pf-c-avatar"
src="${ifDefined(u.user.avatar)}"
alt=""
/>`;
}),
html``,
)}
</a>
<a
href="${globalAK().api.base}flows/-/default/invalidation/"
class="pf-c-nav__link user-logout"
id="logout"
>
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</a>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-sidebar-user": SidebarUser;
}
}

View File

@ -0,0 +1,61 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AdminApi, LicenseSummaryStatusEnum, Version } from "@goauthentik/api";
@customElement("ak-sidebar-version")
export class SidebarVersion extends WithLicenseSummary(AKElement) {
static get styles(): CSSResult[] {
return [
PFBase,
PFNav,
PFAvatar,
css`
:host {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
padding: 1rem !important;
}
p {
text-align: center;
width: 100%;
font-size: var(--pf-global--FontSize--xs);
}
`,
];
}
@state()
version?: Version;
async firstUpdated() {
this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
}
render(): TemplateResult {
let product = globalAK().brand.brandingTitle;
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<p class="pf-c-title">${product}</p>
<p class="pf-c-title">${msg(str`Version ${this.version?.versionCurrent}`)}</p> `;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-sidebar-version": SidebarVersion;
}
}

View File

@ -4,11 +4,11 @@ import {
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_WS_MESSAGE, EVENT_WS_MESSAGE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay } from "@goauthentik/common/ui/config"; import { UIConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { EnterpriseAwareInterface } from "@goauthentik/elements/Interface"; import { EnterpriseAwareInterface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
@ -25,7 +25,6 @@ import "@goauthentik/elements/sidebar/SidebarItem";
import { themeImage } from "@goauthentik/elements/utils/images"; import { themeImage } from "@goauthentik/elements/utils/images";
import { ROUTES } from "@goauthentik/user/Routes"; import { ROUTES } from "@goauthentik/user/Routes";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { match } from "ts-pattern";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
@ -41,7 +40,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import { CoreApi, CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api"; import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api";
const customStyles = css` const customStyles = css`
.pf-c-page__main, .pf-c-page__main,
@ -152,14 +151,6 @@ class UserInterfacePresentation extends AKElement {
@property({ type: Object }) @property({ type: Object })
brand!: CurrentBrand; brand!: CurrentBrand;
get userDisplayName() {
return match<UserDisplay, string>(this.uiConfig.navbar.userDisplay)
.with(UserDisplay.username, () => this.me.user.username)
.with(UserDisplay.name, () => this.me.user.name)
.with(UserDisplay.email, () => this.me.user.email || "")
.otherwise(() => this.me.user.username);
}
get canAccessAdmin() { get canAccessAdmin() {
return ( return (
this.me.user.isSuperuser || this.me.user.isSuperuser ||
@ -172,6 +163,19 @@ class UserInterfacePresentation extends AKElement {
return Boolean(this.uiConfig && this.me && this.brand); return Boolean(this.uiConfig && this.me && this.brand);
} }
renderAdminInterfaceLink() {
if (!this.canAccessAdmin) {
return nothing;
}
return html`<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="/if/admin/"
slot="extra"
>
${msg("Admin interface")}
</a>`;
}
render() { render() {
// The `!` in the field definitions above only re-assure typescript and eslint that the // The `!` in the field definitions above only re-assure typescript and eslint that the
// values *should* be available, not that they *are*. Thus this contract check; it asserts // values *should* be available, not that they *are*. Thus this contract check; it asserts
@ -181,7 +185,7 @@ class UserInterfacePresentation extends AKElement {
throw new Error("ak-interface-user-presentation misused; no valid values passed"); throw new Error("ak-interface-user-presentation misused; no valid values passed");
} }
return html` <ak-locale-context> return html`<ak-locale-context>
<ak-enterprise-status interface="user"></ak-enterprise-status> <ak-enterprise-status interface="user"></ak-enterprise-status>
<div class="pf-c-page"> <div class="pf-c-page">
<div class="background-wrapper" style="${this.uiConfig.theme.background}"> <div class="background-wrapper" style="${this.uiConfig.theme.background}">
@ -199,39 +203,9 @@ class UserInterfacePresentation extends AKElement {
/> />
</a> </a>
</div> </div>
<div class="pf-c-page__header-tools"> <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}
<div class="pf-c-page__header-tools-group"> >${this.renderAdminInterfaceLink()}</ak-nav-buttons
${this.renderApiDrawerTrigger()} >
<!-- -->
${this.renderNotificationDrawerTrigger()}
<!-- -->
${this.renderSettings()}
<div class="pf-c-page__header-tools-item">
<a
href="${globalAK().api.base}flows/-/default/invalidation/"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Sign out")}>
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</pf-tooltip>
</a>
</div>
${this.renderAdminInterfaceLink()}
</div>
${this.renderImpersonation()}
<div class="pf-c-page__header-tools-group">
<div
class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"
>
${this.userDisplayName}
</div>
</div>
<img
class="pf-c-avatar"
src=${this.me.user.avatar}
alt="${msg("Avatar image")}"
/>
</div>
</header> </header>
<div class="pf-c-page__drawer"> <div class="pf-c-page__drawer">
<div <div
@ -274,108 +248,6 @@ class UserInterfacePresentation extends AKElement {
</div> </div>
</ak-locale-context>`; </ak-locale-context>`;
} }
renderApiDrawerTrigger() {
if (!this.uiConfig.enabledFeatures.apiDrawer) {
return nothing;
}
const onClick = (ev: Event) => {
ev.stopPropagation();
this.dispatchEvent(
new Event(EVENT_API_DRAWER_TOGGLE, { bubbles: true, composed: true }),
);
};
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
<button class="pf-c-button pf-m-plain" type="button" @click=${onClick}>
<pf-tooltip position="top" content=${msg("Open API drawer")}>
<i class="fas fa-code" aria-hidden="true"></i>
</pf-tooltip>
</button>
</div>`;
}
renderNotificationDrawerTrigger() {
if (!this.uiConfig.enabledFeatures.notificationDrawer) {
return nothing;
}
const onClick = (ev: Event) => {
ev.stopPropagation();
this.dispatchEvent(
new Event(EVENT_NOTIFICATION_DRAWER_TOGGLE, { bubbles: true, composed: true }),
);
};
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
<button
class="pf-c-button pf-m-plain"
type="button"
aria-label="${msg("Unread notifications")}"
@click=${onClick}
>
<span
class="pf-c-notification-badge ${this.notificationsCount > 0
? "pf-m-unread"
: ""}"
>
<pf-tooltip position="top" content=${msg("Open Notification drawer")}>
<i class="fas fa-bell" aria-hidden="true"></i>
</pf-tooltip>
<span class="pf-c-notification-badge__count">${this.notificationsCount}</span>
</span>
</button>
</div> `;
}
renderSettings() {
if (!this.uiConfig.enabledFeatures.settings) {
return nothing;
}
return html` <div class="pf-c-page__header-tools-item">
<a class="pf-c-button pf-m-plain" type="button" href="#/settings">
<pf-tooltip position="top" content=${msg("Settings")}>
<i class="fas fa-cog" aria-hidden="true"></i>
</pf-tooltip>
</a>
</div>`;
}
renderAdminInterfaceLink() {
if (!this.canAccessAdmin) {
return nothing;
}
return html`<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/admin/"
>
${msg("Admin interface")}
</a>`;
}
renderImpersonation() {
if (!this.me.original) {
return nothing;
}
const onClick = () => {
return new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
};
return html`&nbsp;
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
<ak-action-button class="pf-m-warning pf-m-small" .apiRequest=${onClick}>
${msg("Stop impersonation")}
</ak-action-button>
</div>
</div>`;
}
} }
// ___ _ // ___ _