diff --git a/web/src/admin/admin-overview/charts/OutpostStatusChart.ts b/web/src/admin/admin-overview/charts/OutpostStatusChart.ts index c42193b0db..f62535bc84 100644 --- a/web/src/admin/admin-overview/charts/OutpostStatusChart.ts +++ b/web/src/admin/admin-overview/charts/OutpostStatusChart.ts @@ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart { }), ); this.centerText = outposts.pagination.count.toString(); + outpostStats.sort((a, b) => a.label.localeCompare(b.label)); return outpostStats; } diff --git a/web/src/elements/Base.ts b/web/src/elements/Base.ts index 18021242be..3f6fd2b899 100644 --- a/web/src/elements/Base.ts +++ b/web/src/elements/Base.ts @@ -1,4 +1,5 @@ import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; +import { globalAK } from "@goauthentik/common/global"; import { UIConfig } from "@goauthentik/common/ui/config"; import { adaptCSS } from "@goauthentik/common/utils"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; @@ -16,6 +17,7 @@ type AkInterface = HTMLElement & { brand?: CurrentBrand; uiConfig?: UIConfig; config?: Config; + get activeTheme(): UiThemeEnum | undefined; }; export const rootInterface = (): T | undefined => @@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise { return css; } -const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; +export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; + +// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the +// when changing themes we might not remove the correct css stylesheet instance. +const _darkTheme = ensureCSSStyleSheet(ThemeDark); @localized() export class AKElement extends LitElement { @@ -77,8 +83,7 @@ export class AKElement extends LitElement { } async getTheme(): Promise { - // return rootInterface()?.getTheme() || UiThemeEnum.Automatic; - return rootInterface()?.getTheme() || UiThemeEnum.Light; + return rootInterface()?.getTheme() || UiThemeEnum.Automatic; } fixElementStyles() { @@ -91,9 +96,7 @@ export class AKElement extends LitElement { async _initTheme(root: DocumentOrShadowRoot): Promise { // Early activate theme based on media query to prevent light flash // when dark is preferred - // const pref = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches ? UiThemeEnum.Light : UiThemeEnum.Dark; - // this._activateTheme(root, pref); - this._activateTheme(root, UiThemeEnum.Light); + this._applyTheme(root, globalAK().brand.uiTheme); this._applyTheme(root, await this.getTheme()); } @@ -125,6 +128,7 @@ export class AKElement extends LitElement { : UiThemeEnum.Dark; this._activateTheme(root, theme); }; + this._mediaMatcherHandler(undefined); this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler); } return; @@ -139,7 +143,7 @@ export class AKElement extends LitElement { static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined { if (theme === UiThemeEnum.Dark) { - return ThemeDark; + return _darkTheme; } return undefined; } diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts index 8bedbe16e7..d7fc811f7e 100644 --- a/web/src/elements/Interface/Interface.ts +++ b/web/src/elements/Interface/Interface.ts @@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api"; -import { AKElement } from "../Base"; +import { AKElement, rootInterface } from "../Base"; import { BrandContextController } from "./BrandContextController"; import { ConfigContextController } from "./ConfigContextController"; import { EnterpriseContextController } from "./EnterpriseContextController"; @@ -51,8 +51,14 @@ export class Interface extends AKElement implements AkInterface { } _activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void { - super._activateTheme(root, theme); + if (theme === this._activeTheme) { + return; + } + console.debug( + `authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`, + ); super._activateTheme(document as unknown as DocumentOrShadowRoot, theme); + super._activateTheme(root, theme); } async getTheme(): Promise { diff --git a/web/src/elements/ak-locale-context/ak-locale-context.ts b/web/src/elements/ak-locale-context/ak-locale-context.ts index 0babe24f4d..67a91374c3 100644 --- a/web/src/elements/ak-locale-context/ak-locale-context.ts +++ b/web/src/elements/ak-locale-context/ak-locale-context.ts @@ -1,7 +1,8 @@ import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; +import { AKElement } from "@goauthentik/elements/Base"; import { customEvent } from "@goauthentik/elements/utils/customEvents"; -import { LitElement, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { WithBrandConfig } from "../Interface/brandProvider"; @@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale"; import type { LocaleGetter, LocaleSetter } from "./configureLocale"; import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; -const LocaleContextBase = WithBrandConfig(LitElement); - /** * A component to manage your locale settings. * @@ -25,7 +24,7 @@ const LocaleContextBase = WithBrandConfig(LitElement); * @fires ak-locale-change - When a valid locale has been swapped in */ @customElement("ak-locale-context") -export class LocaleContext extends LocaleContextBase { +export class LocaleContext extends WithBrandConfig(AKElement) { /// @attribute The text representation of the current locale */ @property({ attribute: true, type: String }) locale = DEFAULT_LOCALE; @@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase { return; } locale.locale().then(() => { - console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`); + console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`); this.setLocale(locale.code).then(() => { window.setTimeout(this.notifyApplication, 0); }); diff --git a/web/src/elements/sidebar/SidebarBrand.ts b/web/src/elements/sidebar/SidebarBrand.ts index c08b1063ff..eb36024199 100644 --- a/web/src/elements/sidebar/SidebarBrand.ts +++ b/web/src/elements/sidebar/SidebarBrand.ts @@ -1,6 +1,7 @@ import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; import { AKElement } from "@goauthentik/elements/Base"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; +import { themeImage } from "@goauthentik/elements/utils/images"; import { CSSResult, TemplateResult, css, html } from "lit"; import { customElement } from "lit/decorators.js"; @@ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
authentik Logo diff --git a/web/src/elements/utils/images.ts b/web/src/elements/utils/images.ts new file mode 100644 index 0000000000..8b28d405ce --- /dev/null +++ b/web/src/elements/utils/images.ts @@ -0,0 +1,13 @@ +import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base"; + +import { UiThemeEnum } from "@goauthentik/api"; + +export function themeImage(rawPath: string) { + let enabledTheme = rootInterface()?.activeTheme; + if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) { + enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches + ? UiThemeEnum.Light + : UiThemeEnum.Dark; + } + return rawPath.replace("%(theme)s", enabledTheme); +} diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index 4a3ce9e37b..20c8a7c6e6 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -11,6 +11,7 @@ import { WebsocketClient } from "@goauthentik/common/ws"; import { Interface } from "@goauthentik/elements/Interface"; import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/ak-locale-context"; +import { themeImage } from "@goauthentik/elements/utils/images"; import "@goauthentik/flow/sources/apple/AppleLoginInit"; import "@goauthentik/flow/sources/plex/PlexLoginInit"; import "@goauthentik/flow/stages/FlowErrorStage"; @@ -430,7 +431,9 @@ export class FlowExecutor extends Interface implements StageHost { renderChallengeWrapper(): TemplateResult { const logo = html``; diff --git a/web/src/standalone/api-browser/index.ts b/web/src/standalone/api-browser/index.ts index ce5e017dfc..3272c63d09 100644 --- a/web/src/standalone/api-browser/index.ts +++ b/web/src/standalone/api-browser/index.ts @@ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils"; import { Interface } from "@goauthentik/elements/Interface"; import "@goauthentik/elements/ak-locale-context"; import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; +import { themeImage } from "@goauthentik/elements/utils/images"; import "rapidoc"; import { CSSResult, TemplateResult, css, html } from "lit"; @@ -103,7 +104,9 @@ export class APIBrowser extends Interface {
diff --git a/web/src/user/UserInterface.ts b/web/src/user/UserInterface.ts index 0dd3130bea..e691e90b42 100644 --- a/web/src/user/UserInterface.ts +++ b/web/src/user/UserInterface.ts @@ -21,6 +21,7 @@ import "@goauthentik/elements/router/RouterOutlet"; import "@goauthentik/elements/sidebar/Sidebar"; import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; 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"; @@ -193,7 +194,7 @@ class UserInterfacePresentation extends AKElement {
${this.brand.brandingTitle} diff --git a/website/docs/core/brands.md b/website/docs/core/brands.md index 0a9c2eec23..3fc29a352d 100644 --- a/website/docs/core/brands.md +++ b/website/docs/core/brands.md @@ -21,6 +21,10 @@ This means that if you want to select a default flow based on policy, you can le The brand configuration controls the branding title (shown in website document title and several other places), the sidebar/header logo that appears in the upper left of the product interface, and the favicon on a browser tab. +:::info +Starting with authentik 2024.6.2, the placeholder `%(theme)s` can be used in the logo configuration option, which will be replaced with the active theme. +::: + ## External user settings The **Default application** configuration can be used to redirect external users to an application when they successfully authenticate without being sent from a specific application.