Add switch to toggle dark mode (#33822)
Closes #33821 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Signed-off-by: Jon Koops <jonkoops@gmail.com> Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
0d9d2908f1
commit
19ef0a608b
12 changed files with 93 additions and 193 deletions
|
@ -9,6 +9,8 @@ If you are using a custom theme that extends any of the `keycloak` themes and ar
|
|||
darkMode=false
|
||||
----
|
||||
|
||||
Alternatively, you can disable dark mode support for the built-in Keycloak themes on a per-realm basis by turning off the "Dark mode" setting under the "Theme" tab in the realm settings.
|
||||
|
||||
= LDAP users are created as enabled by default when using Microsoft Active Directory
|
||||
|
||||
If you are using Microsoft AD and creating users through the administrative interfaces, the user will created as enabled by default.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<base href="${resourceUrl}/">
|
||||
<link rel="icon" type="${properties.favIconType!'image/svg+xml'}" href="${resourceUrl}${properties.favIcon!'/favicon.svg'}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light${(properties.darkMode)?boolean?then(' dark', '')}">
|
||||
<meta name="color-scheme" content="light${darkMode?then(' dark', '')}">
|
||||
<meta name="description" content="${properties.description!'The Account Console is a web-based interface for managing your account.'}">
|
||||
<title>${properties.title!'Account Management'}</title>
|
||||
<style>
|
||||
|
@ -58,7 +58,7 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<#if properties.darkMode?boolean>
|
||||
<#if darkMode>
|
||||
<script type="module" async blocking="render">
|
||||
const DARK_MODE_CLASS = "${properties.kcDarkModeClass}";
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
|
|
@ -24,13 +24,13 @@ export default class RealmSettingsPage extends CommonPage {
|
|||
userProfileTab = "rs-user-profile-tab";
|
||||
tokensTab = "rs-tokens-tab";
|
||||
selectLoginTheme = "#kc-login-theme";
|
||||
loginThemeList = "[data-testid='select-login-theme']";
|
||||
loginThemeList = "[data-testid='select-loginTheme']";
|
||||
selectAccountTheme = "#kc-account-theme";
|
||||
accountThemeList = "[data-testid='select-account-theme']";
|
||||
accountThemeList = "[data-testid='select-accountTheme']";
|
||||
selectAdminTheme = "#kc-admin-ui-theme";
|
||||
adminThemeList = "[data-testid='select-admin-theme']";
|
||||
adminThemeList = "[data-testid='select-adminTheme']";
|
||||
selectEmailTheme = "#kc-email-theme";
|
||||
emailThemeList = "[data-testid='select-email-theme']";
|
||||
emailThemeList = "[data-testid='select-emailTheme']";
|
||||
ssoSessionIdleSelectMenu = "#kc-sso-session-idle-select-menu";
|
||||
ssoSessionIdleSelectMenuList = "#kc-sso-session-idle-select-menu ul";
|
||||
ssoSessionMaxSelectMenu = "#kc-sso-session-max-select-menu";
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<base href="${resourceUrl}/">
|
||||
<link rel="icon" type="${properties.favIconType!'image/svg+xml'}" href="${resourceUrl}${properties.favIcon!'/favicon.svg'}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light${(properties.darkMode)?boolean?then(' dark', '')}">
|
||||
<meta name="color-scheme" content="light${darkMode?then(' dark', '')}">
|
||||
<meta name="description" content="${properties.description!'The Keycloak Administration Console is a web-based interface for managing Keycloak.'}">
|
||||
<title>${properties.title!'Keycloak Administration Console'}</title>
|
||||
<style>
|
||||
|
@ -60,7 +60,7 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<#if properties.darkMode?boolean>
|
||||
<#if darkMode>
|
||||
<script type="module" async blocking="render">
|
||||
const DARK_MODE_CLASS = "${properties.kcDarkModeClass}";
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
|
|
@ -3273,6 +3273,8 @@ groupDuplicated=Group duplicated
|
|||
duplicateAGroup=Duplicate group
|
||||
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
|
||||
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
|
||||
darkModeEnabled=Dark mode
|
||||
darkModeEnabledHelp=If enabled the dark variant of the theme will be applied based on user preference through an operating system setting (e.g. light or dark mode) or a user agent setting, if disabled only the light variant will be used. This setting only applies to themes that support dark and light variants, on themes that do not support this feature it will have no effect.
|
||||
showMemberships=Show memberships
|
||||
showMembershipsTitle={{username}} Group Memberships
|
||||
noGroupMembershipsText=This user is not a member of any groups.
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import {
|
||||
HelpItem,
|
||||
KeycloakSelect,
|
||||
SelectVariant,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
SelectOption,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { SelectControl } from "@keycloak/keycloak-ui-shared";
|
||||
import { ActionGroup, Button, PageSection } from "@patternfly/react-core";
|
||||
import { useEffect } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormAccess } from "../components/form/FormAccess";
|
||||
import { DefaultSwitchControl } from "../components/SwitchControl";
|
||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import { convertToFormValues } from "../util";
|
||||
|
||||
|
@ -29,12 +20,8 @@ export const RealmSettingsThemesTab = ({
|
|||
}: RealmSettingsThemesTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
|
||||
const [accountThemeOpen, setAccountThemeOpen] = useState(false);
|
||||
const [adminUIThemeOpen, setAdminUIThemeOpen] = useState(false);
|
||||
const [emailThemeOpen, setEmailThemeOpen] = useState(false);
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm<RealmRepresentation>();
|
||||
const form = useForm<RealmRepresentation>();
|
||||
const { handleSubmit, setValue } = form;
|
||||
const themeTypes = useServerInfo().themes!;
|
||||
|
||||
const setupForm = () => {
|
||||
|
@ -42,6 +29,11 @@ export const RealmSettingsThemesTab = ({
|
|||
};
|
||||
useEffect(setupForm, []);
|
||||
|
||||
const appendEmptyChoice = (items: { key: string; value: string }[]) => [
|
||||
{ key: "", value: t("choose") },
|
||||
...items,
|
||||
];
|
||||
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
|
@ -50,178 +42,70 @@ export const RealmSettingsThemesTab = ({
|
|||
className="pf-v5-u-mt-lg"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<FormGroup
|
||||
label={t("loginTheme")}
|
||||
fieldId="kc-login-theme"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("loginThemeHelp")}
|
||||
fieldLabelId="loginTheme"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
<FormProvider {...form}>
|
||||
<DefaultSwitchControl
|
||||
name="attributes.darkMode"
|
||||
labelIcon={t("darkModeEnabledHelp")}
|
||||
label={t("darkModeEnabled")}
|
||||
defaultValue="true"
|
||||
stringify
|
||||
/>
|
||||
<SelectControl
|
||||
id="kc-login-theme"
|
||||
name="loginTheme"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<KeycloakSelect
|
||||
toggleId="kc-login-theme"
|
||||
onToggle={() => setLoginThemeOpen(!loginThemeOpen)}
|
||||
onSelect={(value) => {
|
||||
field.onChange(value as string);
|
||||
setLoginThemeOpen(false);
|
||||
}}
|
||||
selections={field.value}
|
||||
variant={SelectVariant.single}
|
||||
isOpen={loginThemeOpen}
|
||||
placeholderText={t("selectATheme")}
|
||||
data-testid="select-login-theme"
|
||||
aria-label={t("selectLoginTheme")}
|
||||
>
|
||||
{themeTypes.login.map((theme, idx) => (
|
||||
<SelectOption
|
||||
selected={theme.name === field.value}
|
||||
key={`login-theme-${idx}`}
|
||||
value={theme.name}
|
||||
>
|
||||
{theme.name}
|
||||
</SelectOption>
|
||||
))}
|
||||
</KeycloakSelect>
|
||||
label={t("loginTheme")}
|
||||
labelIcon={t("loginThemeHelp")}
|
||||
controller={{ defaultValue: "" }}
|
||||
options={appendEmptyChoice(
|
||||
themeTypes.login.map((theme) => ({
|
||||
key: theme.name,
|
||||
value: theme.name,
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("accountTheme")}
|
||||
fieldId="kc-account-theme"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("accountThemeHelp")}
|
||||
fieldLabelId="accountTheme"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
<SelectControl
|
||||
id="kc-account-theme"
|
||||
name="accountTheme"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<KeycloakSelect
|
||||
toggleId="kc-account-theme"
|
||||
onToggle={() => setAccountThemeOpen(!accountThemeOpen)}
|
||||
onSelect={(value) => {
|
||||
field.onChange(value as string);
|
||||
setAccountThemeOpen(false);
|
||||
}}
|
||||
selections={field.value}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("selectAccountTheme")}
|
||||
isOpen={accountThemeOpen}
|
||||
placeholderText={t("selectATheme")}
|
||||
data-testid="select-account-theme"
|
||||
>
|
||||
{themeTypes.account
|
||||
.filter((theme) => theme.name !== "base")
|
||||
.map((theme, idx) => (
|
||||
<SelectOption
|
||||
selected={theme.name === field.value}
|
||||
key={`account-theme-${idx}`}
|
||||
value={theme.name}
|
||||
>
|
||||
{t(theme.name)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</KeycloakSelect>
|
||||
label={t("accountTheme")}
|
||||
labelIcon={t("accountThemeHelp")}
|
||||
placeholderText={t("selectATheme")}
|
||||
controller={{ defaultValue: "" }}
|
||||
options={appendEmptyChoice(
|
||||
themeTypes.account.map((theme) => ({
|
||||
key: theme.name,
|
||||
value: theme.name,
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("adminTheme")}
|
||||
fieldId="kc-admin-ui-theme"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("adminThemeHelp")}
|
||||
fieldLabelId="adminTheme"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
<SelectControl
|
||||
id="kc-admin-theme"
|
||||
name="adminTheme"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<KeycloakSelect
|
||||
toggleId="kc-admin-ui-theme"
|
||||
onToggle={() => setAdminUIThemeOpen(!adminUIThemeOpen)}
|
||||
onSelect={(value) => {
|
||||
field.onChange(value as string);
|
||||
setAdminUIThemeOpen(false);
|
||||
}}
|
||||
selections={field.value}
|
||||
variant={SelectVariant.single}
|
||||
isOpen={adminUIThemeOpen}
|
||||
placeholderText={t("selectATheme")}
|
||||
data-testid="select-admin-theme"
|
||||
aria-label={t("selectAdminTheme")}
|
||||
>
|
||||
{themeTypes.admin
|
||||
.filter((theme) => theme.name !== "base")
|
||||
.map((theme, idx) => (
|
||||
<SelectOption
|
||||
selected={theme.name === field.value}
|
||||
key={`admin-theme-${idx}`}
|
||||
value={theme.name}
|
||||
>
|
||||
{t(theme.name)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</KeycloakSelect>
|
||||
label={t("adminTheme")}
|
||||
labelIcon={t("adminThemeHelp")}
|
||||
placeholderText={t("selectATheme")}
|
||||
controller={{ defaultValue: "" }}
|
||||
options={appendEmptyChoice(
|
||||
themeTypes.admin.map((theme) => ({
|
||||
key: theme.name,
|
||||
value: theme.name,
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("emailTheme")}
|
||||
fieldId="kc-email-theme"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("emailThemeHelp")}
|
||||
fieldLabelId="emailTheme"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
<SelectControl
|
||||
id="kc-email-theme"
|
||||
name="emailTheme"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<KeycloakSelect
|
||||
toggleId="kc-email-theme"
|
||||
onToggle={() => setEmailThemeOpen(!emailThemeOpen)}
|
||||
onSelect={(value) => {
|
||||
field.onChange(value as string);
|
||||
setEmailThemeOpen(false);
|
||||
}}
|
||||
selections={field.value}
|
||||
variant={SelectVariant.single}
|
||||
isOpen={emailThemeOpen}
|
||||
placeholderText={t("selectATheme")}
|
||||
data-testid="select-email-theme"
|
||||
aria-label={t("selectEmailTheme")}
|
||||
>
|
||||
{themeTypes.email.map((theme, idx) => (
|
||||
<SelectOption
|
||||
selected={theme.name === field.value}
|
||||
key={`email-theme-${idx}`}
|
||||
value={theme.name}
|
||||
>
|
||||
{t(theme.name)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</KeycloakSelect>
|
||||
label={t("emailTheme")}
|
||||
labelIcon={t("emailThemeHelp")}
|
||||
placeholderText={t("selectATheme")}
|
||||
controller={{ defaultValue: "" }}
|
||||
options={appendEmptyChoice(
|
||||
themeTypes.email.map((theme) => ({
|
||||
key: theme.name,
|
||||
value: theme.name,
|
||||
})),
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormProvider>
|
||||
<ActionGroup>
|
||||
<Button variant="primary" type="submit" data-testid="themes-tab-save">
|
||||
{t("save")}
|
||||
|
|
|
@ -94,7 +94,7 @@ export const SingleSelectControl = <
|
|||
}}
|
||||
isOpen={open}
|
||||
>
|
||||
<SelectList>
|
||||
<SelectList data-testid={`select-${name}`}>
|
||||
{options.map((option) => (
|
||||
<SelectOption key={key(option)} value={key(option)}>
|
||||
{isString(option) ? option : option.value}
|
||||
|
|
|
@ -418,7 +418,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
}
|
||||
|
||||
try {
|
||||
attributes.put("properties", theme.getProperties());
|
||||
Properties properties = theme.getProperties();
|
||||
attributes.put("properties", properties);
|
||||
attributes.put("darkMode", "true".equals(properties.getProperty("darkMode"))
|
||||
&& realm.getAttribute("darkMode", true));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to load properties", e);
|
||||
}
|
||||
|
|
|
@ -194,6 +194,8 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
|
|||
String errorMessage = messagesBundle.getProperty(errorKey);
|
||||
|
||||
attributes.put("message", new MessageBean(errorMessage, MessageType.ERROR));
|
||||
// Default fallback in case an error occurs determining the dark mode later on.
|
||||
attributes.put("darkMode", true);
|
||||
|
||||
try {
|
||||
attributes.put("msg", new MessageFormatterMethod(locale, theme.getMessages(locale)));
|
||||
|
@ -202,7 +204,10 @@ public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
|
|||
}
|
||||
|
||||
try {
|
||||
attributes.put("properties", theme.getProperties());
|
||||
Properties properties = theme.getProperties();
|
||||
attributes.put("properties", properties);
|
||||
attributes.put("darkMode", "true".equals(properties.getProperty("darkMode"))
|
||||
&& realm.getAttribute("darkMode", true));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
|
@ -159,6 +159,8 @@ public class AccountConsole implements AccountResourceProvider {
|
|||
map.put("msgJSON", messagesToJsonString(messages));
|
||||
map.put("supportedLocales", supportedLocales(messages));
|
||||
map.put("properties", theme.getProperties());
|
||||
map.put("darkMode", "true".equals(theme.getProperties().getProperty("darkMode"))
|
||||
&& realm.getAttribute("darkMode", true));
|
||||
map.put("theme", (Function<String, String>) file -> {
|
||||
try {
|
||||
final InputStream resource = theme.getResourceAsStream(file);
|
||||
|
|
|
@ -367,6 +367,8 @@ public class AdminConsole {
|
|||
map.put("loginRealm", realm.getName());
|
||||
map.put("clientId", Constants.ADMIN_CONSOLE_CLIENT_ID);
|
||||
map.put("properties", theme.getProperties());
|
||||
map.put("darkMode", "true".equals(theme.getProperties().getProperty("darkMode"))
|
||||
&& realm.getAttribute("darkMode", true));
|
||||
|
||||
final var devServerUrl = Environment.isDevMode() ? System.getenv(ViteManifest.ADMIN_VITE_URL) : null;
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="color-scheme" content="light${(properties.darkMode)?boolean?then(' dark', '')}">
|
||||
<meta name="color-scheme" content="light${darkMode?then(' dark', '')}">
|
||||
|
||||
<#if properties.meta?has_content>
|
||||
<#list properties.meta?split(' ') as meta>
|
||||
|
@ -55,7 +55,7 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<#if properties.darkMode?boolean>
|
||||
<#if darkMode>
|
||||
<script type="module" async blocking="render">
|
||||
const DARK_MODE_CLASS = "${properties.kcDarkModeClass}";
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
|
Loading…
Reference in a new issue