web: fix dark theme and theme switch (#10667)

* base locale off of ak-element

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

* revert temp theme fixes

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

* fix theme switching

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

* add basic support for theme-different images

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

* sort outposts in card

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

* set default theme based on pre-hydrated brand settings

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

* activate global theme before root in shadow dom

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

* logging

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

* when using _applyTheme, check media matcher

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

* add docs

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2024-07-29 20:00:25 +02:00
committed by GitHub
parent 2a70b4aae2
commit 7f0c6ddb5b
10 changed files with 53 additions and 18 deletions

View File

@ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
}), }),
); );
this.centerText = outposts.pagination.count.toString(); this.centerText = outposts.pagination.count.toString();
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
return outpostStats; return outpostStats;
} }

View File

@ -1,4 +1,5 @@
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { UIConfig } from "@goauthentik/common/ui/config"; import { UIConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils"; import { adaptCSS } from "@goauthentik/common/utils";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
@ -16,6 +17,7 @@ type AkInterface = HTMLElement & {
brand?: CurrentBrand; brand?: CurrentBrand;
uiConfig?: UIConfig; uiConfig?: UIConfig;
config?: Config; config?: Config;
get activeTheme(): UiThemeEnum | undefined;
}; };
export const rootInterface = <T extends AkInterface>(): T | undefined => export const rootInterface = <T extends AkInterface>(): T | undefined =>
@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> {
return css; 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() @localized()
export class AKElement extends LitElement { export class AKElement extends LitElement {
@ -77,8 +83,7 @@ export class AKElement extends LitElement {
} }
async getTheme(): Promise<UiThemeEnum> { async getTheme(): Promise<UiThemeEnum> {
// return rootInterface()?.getTheme() || UiThemeEnum.Automatic; return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
return rootInterface()?.getTheme() || UiThemeEnum.Light;
} }
fixElementStyles() { fixElementStyles() {
@ -91,9 +96,7 @@ export class AKElement extends LitElement {
async _initTheme(root: DocumentOrShadowRoot): Promise<void> { async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
// Early activate theme based on media query to prevent light flash // Early activate theme based on media query to prevent light flash
// when dark is preferred // when dark is preferred
// const pref = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches ? UiThemeEnum.Light : UiThemeEnum.Dark; this._applyTheme(root, globalAK().brand.uiTheme);
// this._activateTheme(root, pref);
this._activateTheme(root, UiThemeEnum.Light);
this._applyTheme(root, await this.getTheme()); this._applyTheme(root, await this.getTheme());
} }
@ -125,6 +128,7 @@ export class AKElement extends LitElement {
: UiThemeEnum.Dark; : UiThemeEnum.Dark;
this._activateTheme(root, theme); this._activateTheme(root, theme);
}; };
this._mediaMatcherHandler(undefined);
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler); this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
} }
return; return;
@ -139,7 +143,7 @@ export class AKElement extends LitElement {
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined { static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
if (theme === UiThemeEnum.Dark) { if (theme === UiThemeEnum.Dark) {
return ThemeDark; return _darkTheme;
} }
return undefined; return undefined;
} }

View File

@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api"; import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
import { AKElement } from "../Base"; import { AKElement, rootInterface } from "../Base";
import { BrandContextController } from "./BrandContextController"; import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController"; import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController"; import { EnterpriseContextController } from "./EnterpriseContextController";
@ -51,8 +51,14 @@ export class Interface extends AKElement implements AkInterface {
} }
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void { _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(document as unknown as DocumentOrShadowRoot, theme);
super._activateTheme(root, theme);
} }
async getTheme(): Promise<UiThemeEnum> { async getTheme(): Promise<UiThemeEnum> {

View File

@ -1,7 +1,8 @@
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; 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 { customEvent } from "@goauthentik/elements/utils/customEvents";
import { LitElement, html } from "lit"; import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider"; import { WithBrandConfig } from "../Interface/brandProvider";
@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale";
import type { LocaleGetter, LocaleSetter } from "./configureLocale"; import type { LocaleGetter, LocaleSetter } from "./configureLocale";
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
const LocaleContextBase = WithBrandConfig(LitElement);
/** /**
* A component to manage your locale settings. * 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 * @fires ak-locale-change - When a valid locale has been swapped in
*/ */
@customElement("ak-locale-context") @customElement("ak-locale-context")
export class LocaleContext extends LocaleContextBase { export class LocaleContext extends WithBrandConfig(AKElement) {
/// @attribute The text representation of the current locale */ /// @attribute The text representation of the current locale */
@property({ attribute: true, type: String }) @property({ attribute: true, type: String })
locale = DEFAULT_LOCALE; locale = DEFAULT_LOCALE;
@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase {
return; return;
} }
locale.locale().then(() => { 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(() => { this.setLocale(locale.code).then(() => {
window.setTimeout(this.notifyApplication, 0); window.setTimeout(this.notifyApplication, 0);
}); });

View File

@ -1,6 +1,7 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
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 { themeImage } from "@goauthentik/elements/utils/images";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
@ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
<a href="#/" class="pf-c-page__header-brand-link"> <a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img <img
src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo} src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
alt="authentik Logo" alt="authentik Logo"
loading="lazy" loading="lazy"
/> />

View File

@ -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);
}

View File

@ -11,6 +11,7 @@ import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/LoadingOverlay";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import { themeImage } from "@goauthentik/elements/utils/images";
import "@goauthentik/flow/sources/apple/AppleLoginInit"; import "@goauthentik/flow/sources/apple/AppleLoginInit";
import "@goauthentik/flow/sources/plex/PlexLoginInit"; import "@goauthentik/flow/sources/plex/PlexLoginInit";
import "@goauthentik/flow/stages/FlowErrorStage"; import "@goauthentik/flow/stages/FlowErrorStage";
@ -430,7 +431,9 @@ export class FlowExecutor extends Interface implements StageHost {
renderChallengeWrapper(): TemplateResult { renderChallengeWrapper(): TemplateResult {
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand"> const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img <img
src="${first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, "")}" src="${themeImage(
first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, ""),
)}"
alt="authentik Logo" alt="authentik Logo"
/> />
</div>`; </div>`;

View File

@ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { themeImage } from "@goauthentik/elements/utils/images";
import "rapidoc"; import "rapidoc";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
@ -103,7 +104,9 @@ export class APIBrowser extends Interface {
<img <img
alt="authentik Logo" alt="authentik Logo"
class="logo" class="logo"
src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}" src="${themeImage(
first(this.brand?.brandingLogo, DefaultBrand.brandingLogo),
)}"
/> />
</div> </div>
</rapi-doc> </rapi-doc>

View File

@ -21,6 +21,7 @@ import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar"; import "@goauthentik/elements/sidebar/Sidebar";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarItem"; import "@goauthentik/elements/sidebar/SidebarItem";
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 { match } from "ts-pattern";
@ -193,7 +194,7 @@ class UserInterfacePresentation extends AKElement {
<a href="#/" class="pf-c-page__header-brand-link"> <a href="#/" class="pf-c-page__header-brand-link">
<img <img
class="pf-c-brand" class="pf-c-brand"
src="${this.brand.brandingLogo}" src="${themeImage(this.brand.brandingLogo)}"
alt="${this.brand.brandingTitle}" alt="${this.brand.brandingTitle}"
/> />
</a> </a>

View File

@ -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. 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 ## 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. 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.