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:
@ -7,7 +7,9 @@ from sys import version as python_version
|
||||
from typing import TypedDict
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import backend
|
||||
from django.conf import settings
|
||||
from django.utils.timezone import now
|
||||
from django.views.debug import SafeExceptionReporterFilter
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
@ -52,10 +54,16 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||
"""Get HTTP Request headers"""
|
||||
headers = {}
|
||||
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
for key, value in request.META.items():
|
||||
if not isinstance(value, str):
|
||||
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
|
||||
|
||||
def get_http_host(self, request: Request) -> str:
|
||||
|
13
poetry.lock
generated
13
poetry.lock
generated
@ -5225,6 +5225,17 @@ files = [
|
||||
[package.dependencies]
|
||||
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]]
|
||||
name = "uritemplate"
|
||||
version = "4.1.1"
|
||||
@ -5934,4 +5945,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12"
|
||||
content-hash = "38089ad25be7638c118f4b503ad2f8495c941667f5485efe60b2bbdb14d6f44c"
|
||||
content-hash = "527289cd983966592bc04f308917604dc5d4a4116d8071d5e3a0bf63d216a086"
|
||||
|
@ -143,6 +143,7 @@ swagger-spec-validator = "*"
|
||||
tenant-schemas-celery = "*"
|
||||
twilio = "*"
|
||||
ua-parser = "*"
|
||||
unidecode = "*"
|
||||
# Pinned because of botocore https://github.com/orgs/python-poetry/discussions/7937
|
||||
urllib3 = { extras = ["secure"], version = "<3" }
|
||||
uvicorn = { extras = ["standard"], version = "*" }
|
||||
|
@ -96,7 +96,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
render(): TemplateResult {
|
||||
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>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section">
|
||||
|
@ -3,6 +3,7 @@
|
||||
--ak-global--Color--100: var(--ak-dark-foreground) !important;
|
||||
--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--BorderColor--100: var(--ak-dark-background-lighter) !important;
|
||||
}
|
||||
body {
|
||||
background-color: var(--ak-dark-background) !important;
|
||||
@ -31,9 +32,6 @@ body {
|
||||
.notification-trigger {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.pf-c-page__main-section.pf-m-light {
|
||||
background-color: transparent;
|
||||
}
|
||||
.pf-c-content {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export enum UserDisplay {
|
||||
username = "username",
|
||||
name = "name",
|
||||
email = "email",
|
||||
none = "none",
|
||||
}
|
||||
|
||||
export enum LayoutType {
|
||||
|
210
web/src/components/ak-nav-buttons.ts
Normal file
210
web/src/components/ak-nav-buttons.ts
Normal 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`
|
||||
<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>`;
|
||||
}
|
||||
}
|
@ -1,27 +1,30 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import {
|
||||
EVENT_API_DRAWER_TOGGLE,
|
||||
EVENT_NOTIFICATION_DRAWER_TOGGLE,
|
||||
EVENT_SIDEBAR_TOGGLE,
|
||||
EVENT_WS_MESSAGE,
|
||||
TITLE_DEFAULT,
|
||||
} from "@goauthentik/common/constants";
|
||||
import { currentInterface } from "@goauthentik/common/sentry";
|
||||
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import "@goauthentik/components/ak-nav-buttons";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
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 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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { EventsApi } from "@goauthentik/api";
|
||||
import { EventsApi, SessionUser } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-page-header")
|
||||
export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
@ -31,8 +34,8 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
@property({ type: Boolean })
|
||||
iconImage = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hasNotifications = false;
|
||||
@state()
|
||||
notificationsCount = 0;
|
||||
|
||||
@property()
|
||||
header = "";
|
||||
@ -40,21 +43,39 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
@property()
|
||||
description?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hasIcon = true;
|
||||
|
||||
@state()
|
||||
me?: SessionUser;
|
||||
|
||||
@state()
|
||||
uiConfig!: UIConfig;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFPage,
|
||||
PFNotificationBadge,
|
||||
PFContent,
|
||||
PFAvatar,
|
||||
PFDropdown,
|
||||
css`
|
||||
:host {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.bar {
|
||||
border-bottom: var(--pf-global--BorderWidth--sm);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--pf-global--BorderColor--100);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 114px;
|
||||
}
|
||||
.pf-c-button.pf-m-plain {
|
||||
background-color: transparent;
|
||||
border-radius: 0px;
|
||||
max-height: 114px;
|
||||
background-color: var(--pf-c-page--BackgroundColor);
|
||||
}
|
||||
.pf-c-page__main-section.pf-m-light {
|
||||
background-color: transparent;
|
||||
@ -81,6 +102,15 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
flex-direction: row;
|
||||
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 {
|
||||
me().then((user) => {
|
||||
new EventsApi(DEFAULT_CONFIG)
|
||||
.eventsNotificationsList({
|
||||
async firstUpdated() {
|
||||
this.me = await me();
|
||||
this.uiConfig = await uiConfig();
|
||||
this.uiConfig.navbar.userDisplay = UserDisplay.none;
|
||||
const notifications = await new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
|
||||
seen: false,
|
||||
ordering: "-created",
|
||||
pageSize: 1,
|
||||
user: user.user.pk,
|
||||
})
|
||||
.then((r) => {
|
||||
this.hasNotifications = r.pagination.count > 0;
|
||||
});
|
||||
user: this.me.user.pk,
|
||||
});
|
||||
this.notificationsCount = notifications.pagination.count;
|
||||
}
|
||||
|
||||
setTitle(header?: string) {
|
||||
@ -126,7 +154,7 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
this.setTitle(this.header);
|
||||
}
|
||||
|
||||
renderIcon(): TemplateResult {
|
||||
renderIcon() {
|
||||
if (this.icon) {
|
||||
if (this.iconImage && !this.icon.startsWith("fa://")) {
|
||||
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 ");
|
||||
return html`<i class=${icon}></i>`;
|
||||
}
|
||||
return html``;
|
||||
return nothing;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@ -155,44 +183,19 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<slot name="icon">${this.renderIcon()}</slot>
|
||||
${this.hasIcon
|
||||
? html`<slot name="icon">${this.renderIcon()}</slot> `
|
||||
: nothing}
|
||||
<slot name="header">${this.header}</slot>
|
||||
</h1>
|
||||
${this.description ? html`<p>${this.description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<button
|
||||
class="notification-trigger pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_API_DRAWER_TOGGLE, {
|
||||
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 class="pf-c-page__header-tools">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}></ak-nav-buttons>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/sidebar/SidebarBrand";
|
||||
import "@goauthentik/elements/sidebar/SidebarUser";
|
||||
import "@goauthentik/elements/sidebar/SidebarVersion";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
@ -74,7 +74,7 @@ export class Sidebar extends AKElement {
|
||||
<ul class="pf-c-nav__list">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
<ak-sidebar-user></ak-sidebar-user>
|
||||
<ak-sidebar-version></ak-sidebar-version>
|
||||
</nav>`;
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,9 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
|
||||
align-items: center;
|
||||
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 {
|
||||
padding: 0 0.5rem;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
61
web/src/elements/sidebar/SidebarVersion.ts
Normal file
61
web/src/elements/sidebar/SidebarVersion.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -4,11 +4,11 @@ import {
|
||||
EVENT_NOTIFICATION_DRAWER_TOGGLE,
|
||||
EVENT_WS_MESSAGE,
|
||||
} from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
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 { WebsocketClient } from "@goauthentik/common/ws";
|
||||
import "@goauthentik/components/ak-nav-buttons";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { EnterpriseAwareInterface } from "@goauthentik/elements/Interface";
|
||||
import "@goauthentik/elements/ak-locale-context";
|
||||
@ -25,7 +25,6 @@ import "@goauthentik/elements/sidebar/SidebarItem";
|
||||
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||
import { ROUTES } from "@goauthentik/user/Routes";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
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 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`
|
||||
.pf-c-page__main,
|
||||
@ -152,14 +151,6 @@ class UserInterfacePresentation extends AKElement {
|
||||
@property({ type: Object })
|
||||
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() {
|
||||
return (
|
||||
this.me.user.isSuperuser ||
|
||||
@ -172,6 +163,19 @@ class UserInterfacePresentation extends AKElement {
|
||||
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() {
|
||||
// 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
|
||||
@ -199,39 +203,9 @@ class UserInterfacePresentation extends AKElement {
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<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"
|
||||
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}
|
||||
>${this.renderAdminInterfaceLink()}</ak-nav-buttons
|
||||
>
|
||||
<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>
|
||||
<div class="pf-c-page__drawer">
|
||||
<div
|
||||
@ -274,108 +248,6 @@ class UserInterfacePresentation extends AKElement {
|
||||
</div>
|
||||
</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`
|
||||
<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>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ___ _
|
||||
|
Reference in New Issue
Block a user