mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
Show login page for quick theme and change basic attributes (#45483)
Closes #45524 Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
parent
c17d9d0d0c
commit
5a4e90dfc0
10 changed files with 424 additions and 117 deletions
|
|
@ -2955,6 +2955,8 @@ memberofLdapAttribute=Member-of LDAP attribute
|
|||
supportedLocales=Supported locales
|
||||
invalidLocale=Invalid locale selected
|
||||
showPasswordDataValue=Value
|
||||
adminConsolePreview=Admin Console Preview
|
||||
loginPagePreview=Login Page Preview
|
||||
webAuthnPolicyAttestationConveyancePreference=Attestation conveyance preference
|
||||
copyOf=Copy of {{name}}
|
||||
eventTypes.REMOVE_TOTP.description=Remove totp
|
||||
|
|
@ -3262,6 +3264,8 @@ identityBrokeringLink=Identity brokering link
|
|||
searchClientRegistration=Search for policy
|
||||
importFileHelp=File to import a key
|
||||
logo=Logo
|
||||
logoWidth=Logo width
|
||||
logoHeight=Logo height
|
||||
avatarImage=Avatar image
|
||||
eventTypes.INVITE_ORG.name=Invite user to organization
|
||||
eventTypes.INVITE_ORG.description=Invite user to organization
|
||||
|
|
@ -3751,4 +3755,4 @@ role_default-roles=Default roles
|
|||
role_impersonation=Impersonation
|
||||
role_read-token=Read token
|
||||
role_offline-access=Offline access
|
||||
role_uma_authorization=Obtain permissions
|
||||
role_uma_authorization=Obtain permissions
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
.pf-v5-c-login__container {
|
||||
grid-template-columns: 34rem;
|
||||
grid-template-areas: "header"
|
||||
"main"
|
||||
}
|
||||
|
||||
.login-pf body {
|
||||
background: var(--keycloak-bg-logo-url) no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.kc-logo-text {
|
||||
background-image: var(--keycloak-logo-url);
|
||||
height: var(--keycloak-logo-height);
|
||||
width: var(--keycloak-logo-width);
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div.kc-logo-text span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kc-login-tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.kc-login-tooltip .kc-tooltip-text {
|
||||
top: -3px;
|
||||
left: 160%;
|
||||
background-color: black;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
|
||||
min-width: 130px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
|
||||
padding: 5px;
|
||||
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
/* Show tooltip */
|
||||
.kc-login-tooltip:hover .kc-tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Arrow for tooltip */
|
||||
.kc-login-tooltip .kc-tooltip-text::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 100%;
|
||||
margin-top: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent black transparent transparent;
|
||||
}
|
||||
|
||||
#kc-recovery-codes-list {
|
||||
columns: 2;
|
||||
}
|
||||
|
||||
#certificate_subjectDN {
|
||||
overflow-wrap: break-word
|
||||
}
|
||||
|
||||
#kc-header-wrapper {
|
||||
font-size: 29px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
line-height: 1.2em;
|
||||
white-space: normal;
|
||||
color: var(--pf-v5-global--Color--light-100) !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: var(--pf-v5-global--spacer--sm);
|
||||
margin-bottom: var(--pf-v5-global--spacer--md);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
div.pf-v5-c-login__main-header {
|
||||
grid-template-columns: 70% 30%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { createNamedContext } from "@keycloak/keycloak-ui-shared";
|
||||
import { PropsWithChildren, useContext, useState } from "react";
|
||||
|
||||
type BackgroundContextProps = {
|
||||
background: string;
|
||||
setBackground: (background: string) => void;
|
||||
};
|
||||
|
||||
export const BackgroundPreviewContext = createNamedContext<
|
||||
BackgroundContextProps | undefined
|
||||
>("BackgroundContext", undefined);
|
||||
|
||||
export const usePreviewBackground = () => useContext(BackgroundPreviewContext);
|
||||
|
||||
export const BackgroundContext = ({ children }: PropsWithChildren) => {
|
||||
const [background, setBackground] = useState("");
|
||||
|
||||
return (
|
||||
<BackgroundPreviewContext.Provider value={{ background, setBackground }}>
|
||||
{children}
|
||||
</BackgroundPreviewContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@ import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared";
|
|||
import { FileUpload } from "@patternfly/react-core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { fileToDataUri } from "./fileUtils";
|
||||
|
||||
type ImageUploadProps = {
|
||||
name: string;
|
||||
|
|
@ -15,15 +16,6 @@ export const ImageUpload = ({ name, onChange }: ImageUploadProps) => {
|
|||
|
||||
const { control, watch } = useFormContext();
|
||||
|
||||
const fileToDataUri = (file: File) =>
|
||||
new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
resolve(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
if (file) {
|
||||
void fileToDataUri(file).then((dataUri) => {
|
||||
setDataUri(dataUri);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
import { loginThemeProperties as properties } from "./LoginThemeProperties";
|
||||
import { usePreviewLogo } from "./LogoContext";
|
||||
import { useEnvironment } from "@keycloak/keycloak-ui-shared";
|
||||
import { Environment } from "../../environment";
|
||||
import { usePreviewBackground } from "./BackgroundContext";
|
||||
|
||||
type LoginPreviewWindowProps = {
|
||||
cssVars: Record<string, string>;
|
||||
};
|
||||
|
||||
export const LoginPreviewWindow = ({ cssVars }: LoginPreviewWindowProps) => {
|
||||
const { environment } = useEnvironment<Environment>();
|
||||
const contextLogo = usePreviewLogo();
|
||||
const contextBackground = usePreviewBackground();
|
||||
|
||||
// Resources
|
||||
const resourceUrlRoot = `/resources/${environment.resourceVersion}`;
|
||||
const loginResourceUrl = `${resourceUrlRoot}/login/keycloak.v2`;
|
||||
|
||||
// Default login theme resources from local files
|
||||
const defaultBgImage = `${loginResourceUrl}/img/keycloak-bg-darken.svg`;
|
||||
const defaultLogo = `${loginResourceUrl}/img/keycloak-logo-text.svg`;
|
||||
|
||||
// Use uploaded images or fall back to local defaults
|
||||
// Both logo and background come from context for immediate reactivity
|
||||
const logoUrl = contextLogo?.logo || defaultLogo;
|
||||
const bgUrl = contextBackground?.background || defaultBgImage;
|
||||
|
||||
const logoWidth = cssVars["logoWidth"];
|
||||
const logoHeight = cssVars["logoHeight"];
|
||||
|
||||
// CSS files
|
||||
const pf5CssRootPath = `${resourceUrlRoot}/common/keycloak/vendor/patternfly-v5`;
|
||||
const pf5Css = `${pf5CssRootPath}/patternfly.min.css`;
|
||||
const pf5AddonsCss = `${pf5CssRootPath}/patternfly-addons.css`;
|
||||
|
||||
const stylesThemeCssUrl = `${loginResourceUrl}/css/styles.css`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="stylesheet" href={pf5Css} />
|
||||
<link rel="stylesheet" href={pf5AddonsCss} />
|
||||
<link rel="stylesheet" href={stylesThemeCssUrl} />
|
||||
|
||||
<style>{`
|
||||
.login-preview {
|
||||
${Object.entries(cssVars)
|
||||
.map(([key, value]) => `--pf-v5-global--${key}: ${value};`)
|
||||
.join("\n")}
|
||||
|
||||
/* Keycloak login theme variables - override with local/uploaded images */
|
||||
--keycloak-logo-url: url('${logoUrl}');
|
||||
--keycloak-bg-logo-url: url('${bgUrl}');
|
||||
${logoHeight ? `--keycloak-logo-height: ${logoHeight};` : ""}
|
||||
${logoWidth ? `--keycloak-logo-width: ${logoWidth};` : ""}
|
||||
}
|
||||
|
||||
/* Apply background to #keycloak-bg */
|
||||
.login-preview.${properties.kcHtmlClass} {
|
||||
background: var(--keycloak-bg-logo-url) no-repeat center center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Ensure login container is properly sized */
|
||||
.login-preview .${properties.kcLogin} {
|
||||
min-height:70vh;
|
||||
}
|
||||
|
||||
/* Force single column layout */
|
||||
.login-preview .${properties.kcLoginContainer} {
|
||||
grid-template-columns: 34rem !important;
|
||||
grid-template-areas: "header"
|
||||
"main" !important;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="login-preview-wrapper">
|
||||
<div className={`login-preview ${properties.kcHtmlClass}`}>
|
||||
<div id="keycloak-bg" data-page-id="login-preview">
|
||||
<div className={properties.kcLogin}>
|
||||
<div className={properties.kcLoginContainer}>
|
||||
<header id="kc-header" className="pf-v5-c-login__header">
|
||||
<div id="kc-header-wrapper" className="pf-v5-c-brand">
|
||||
<div className="kc-logo-text">
|
||||
<span>Keycloak</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className={properties.kcLoginMain}>
|
||||
<div className={properties.kcLoginMainHeader}>
|
||||
<h1
|
||||
className={properties.kcLoginMainTitle}
|
||||
id="kc-page-title"
|
||||
>
|
||||
Sign in to your account
|
||||
</h1>
|
||||
</div>
|
||||
<div className={properties.kcLoginMainBody}>
|
||||
<div id="kc-form">
|
||||
<div id="kc-form-wrapper">
|
||||
<form
|
||||
id="kc-form-login"
|
||||
className={properties.kcFormClass}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
noValidate
|
||||
>
|
||||
{/* Username field */}
|
||||
<div className={properties.kcFormGroupClass}>
|
||||
<div className={properties.kcFormGroupLabelClass}>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className={properties.kcFormLabelClass}
|
||||
>
|
||||
<span
|
||||
className={properties.kcFormLabelTextClass}
|
||||
>
|
||||
Username or email
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<span className={properties.kcInputClass}>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
value=""
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
readOnly
|
||||
/>
|
||||
</span>
|
||||
<div id="input-error-container-username"></div>
|
||||
</div>
|
||||
|
||||
{/* Password field */}
|
||||
<div className={properties.kcFormGroupClass}>
|
||||
<div className={properties.kcFormGroupLabelClass}>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className={properties.kcFormLabelClass}
|
||||
>
|
||||
<span
|
||||
className={properties.kcFormLabelTextClass}
|
||||
>
|
||||
Password
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className={properties.kcInputGroup}>
|
||||
<div
|
||||
className={`${properties.kcInputGroupItemClass} ${properties.kcFill}`}
|
||||
>
|
||||
<span className={properties.kcInputClass}>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
value=""
|
||||
type={"password"}
|
||||
autoComplete="current-password"
|
||||
readOnly
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className={properties.kcInputGroupItemClass}>
|
||||
<button
|
||||
className={
|
||||
properties.kcFormPasswordVisibilityButtonClass
|
||||
}
|
||||
type="button"
|
||||
aria-label={"Show password"}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
properties.kcFormPasswordVisibilityIconShow
|
||||
}
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={properties.kcFormHelperTextClass}
|
||||
aria-live="polite"
|
||||
>
|
||||
<div
|
||||
className={properties.kcInputHelperTextClass}
|
||||
></div>
|
||||
</div>
|
||||
<div id="input-error-container-password"></div>
|
||||
</div>
|
||||
|
||||
{/* Submit button */}
|
||||
<div className={properties.kcFormGroupClass}>
|
||||
<div className={properties.kcFormActionGroupClass}>
|
||||
<button
|
||||
className={`${properties.kcButtonPrimaryClass} ${properties.kcButtonBlockClass}`}
|
||||
name="login"
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={properties.kcLoginMainFooter}>
|
||||
{/* Social providers or additional info would go here */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={properties.kcLoginMainFooter}></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// Keycloak v2 Login Theme Properties
|
||||
// Based on theme/keycloak.v2/login/theme.properties
|
||||
|
||||
export const loginThemeProperties = {
|
||||
// Form classes
|
||||
kcFormGroupClass: "pf-v5-c-form__group",
|
||||
kcFormGroupLabelClass: "pf-v5-c-form__group-label pf-v5-u-pb-xs",
|
||||
kcFormLabelClass: "pf-v5-c-form__label",
|
||||
kcFormLabelTextClass: "pf-v5-c-form__label-text",
|
||||
kcLabelClass: "pf-v5-c-form__label",
|
||||
kcInputClass: "pf-v5-c-form-control",
|
||||
kcInputGroup: "pf-v5-c-input-group",
|
||||
kcFormHelperTextClass: "pf-v5-c-form__helper-text",
|
||||
kcInputHelperTextClass:
|
||||
"pf-v5-c-helper-text pf-v5-u-display-flex pf-v5-u-justify-content-space-between",
|
||||
kcInputHelperTextItemClass: "pf-v5-c-helper-text__item",
|
||||
kcInputHelperTextItemTextClass: "pf-v5-c-helper-text__item-text",
|
||||
kcInputGroupItemClass: "pf-v5-c-input-group__item",
|
||||
kcFill: "pf-m-fill",
|
||||
kcError: "pf-m-error",
|
||||
|
||||
// Checkbox classes
|
||||
kcCheckboxClass: "pf-v5-c-check",
|
||||
kcCheckboxInputClass: "pf-v5-c-check__input",
|
||||
kcCheckboxLabelClass: "pf-v5-c-check__label",
|
||||
kcCheckboxLabelRequiredClass: "pf-v5-c-check__label-required",
|
||||
|
||||
// Form control utilities
|
||||
kcInputRequiredClass: "pf-v5-c-form__label-required",
|
||||
kcInputErrorMessageClass:
|
||||
"pf-v5-c-helper-text__item-text pf-m-error kc-feedback-text",
|
||||
kcFormControlUtilClass: "pf-v5-c-form-control__utilities",
|
||||
kcInputErrorIconStatusClass: "pf-v5-c-form-control__icon pf-m-status",
|
||||
kcInputErrorIconClass: "fas fa-exclamation-circle",
|
||||
|
||||
// Alert classes
|
||||
kcAlertClass: "pf-v5-c-alert pf-m-inline pf-v5-u-mb-md",
|
||||
kcAlertIconClass: "pf-v5-c-alert__icon",
|
||||
kcAlertTitleClass: "pf-v5-c-alert__title",
|
||||
kcAlertDescriptionClass: "pf-v5-c-alert__description",
|
||||
|
||||
// Password visibility
|
||||
kcFormPasswordVisibilityButtonClass: "pf-v5-c-button pf-m-control",
|
||||
kcFormPasswordVisibilityIconShow: "fa-eye fas",
|
||||
kcFormPasswordVisibilityIconHide: "fa-eye-slash fas",
|
||||
kcFormControlToggleIcon: "pf-v5-c-form-control__toggle-icon",
|
||||
|
||||
// Form actions
|
||||
kcFormActionGroupClass: "pf-v5-c-form__actions pf-v5-u-pt-xs",
|
||||
kcFormReadOnlyClass: "pf-m-readonly",
|
||||
|
||||
// Button classes
|
||||
kcButtonClass: "pf-v5-c-button",
|
||||
kcButtonPrimaryClass: "pf-v5-c-button pf-m-primary",
|
||||
kcButtonSecondaryClass: "pf-v5-c-button pf-m-secondary",
|
||||
kcButtonBlockClass: "pf-m-block",
|
||||
kcButtonLinkClass: "pf-v5-c-button pf-m-link",
|
||||
|
||||
// Login layout classes
|
||||
kcLogin: "pf-v5-c-login",
|
||||
kcLoginContainer: "pf-v5-c-login__container",
|
||||
kcLoginMain: "pf-v5-c-login__main",
|
||||
kcLoginMainHeader: "pf-v5-c-login__main-header",
|
||||
kcLoginMainFooter: "pf-v5-c-login__main-footer",
|
||||
kcLoginMainFooterBand: "pf-v5-c-login__main-footer-band",
|
||||
kcLoginMainFooterBandItem: "pf-v5-c-login__main-footer-band-item",
|
||||
kcLoginMainFooterHelperText: "pf-v5-u-font-size-sm pf-v5-u-color-200",
|
||||
kcLoginMainTitle: "pf-v5-c-title pf-m-3xl",
|
||||
kcLoginMainHeaderUtilities: "pf-v5-c-login__main-header-utilities",
|
||||
kcLoginMainBody: "pf-v5-c-login__main-body",
|
||||
|
||||
// Form and card classes
|
||||
kcLoginClass: "pf-v5-c-login__main",
|
||||
kcFormClass: "pf-v5-c-form pf-v5-u-w-100",
|
||||
kcFormCardClass: "card-pf",
|
||||
|
||||
// Feedback icons
|
||||
kcFeedbackErrorIcon: "fa fa-fw fa-exclamation-circle",
|
||||
kcFeedbackWarningIcon: "fa fa-fw fa-exclamation-triangle",
|
||||
kcFeedbackSuccessIcon: "fa fa-fw fa-check-circle",
|
||||
kcFeedbackInfoIcon: "fa fa-fw fa-info-circle",
|
||||
|
||||
// Dark mode
|
||||
kcDarkModeClass: "pf-v5-theme-dark",
|
||||
|
||||
// HTML and Body classes
|
||||
kcHtmlClass: "login-pf",
|
||||
kcBodyClass: "",
|
||||
|
||||
// Content wrapper
|
||||
kcContentWrapperClass: "pf-v5-u-mb-md-on-md",
|
||||
};
|
||||
|
||||
export type LoginThemeProperties = typeof loginThemeProperties;
|
||||
|
|
@ -8,6 +8,8 @@ import {
|
|||
InputGroup,
|
||||
InputGroupItem,
|
||||
PageSection,
|
||||
Tab,
|
||||
Tabs,
|
||||
Text,
|
||||
TextContent,
|
||||
TextInputProps,
|
||||
|
|
@ -25,9 +27,12 @@ import { useTranslation } from "react-i18next";
|
|||
import { FixedButtonsGroup } from "../../components/form/FixedButtonGroup";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import { fileToDataUri } from "./fileUtils";
|
||||
import { FileNameDialog } from "./FileNameDialog";
|
||||
import { ImageUpload } from "./ImageUpload";
|
||||
import { LoginPreviewWindow } from "./LoginPreviewWindow";
|
||||
import { usePreviewLogo } from "./LogoContext";
|
||||
import { usePreviewBackground } from "./BackgroundContext";
|
||||
import { darkTheme, lightTheme } from "./PatternflyVars";
|
||||
import { PreviewWindow } from "./PreviewWindow";
|
||||
import { ThemeRealmRepresentation } from "./ThemesTab";
|
||||
|
|
@ -89,7 +94,9 @@ export const ThemeColors = ({
|
|||
const { handleSubmit, watch } = form;
|
||||
const style = watch();
|
||||
const contextLogo = usePreviewLogo();
|
||||
const contextBackground = usePreviewBackground();
|
||||
const [open, toggle, setOpen] = useToggle();
|
||||
const [previewTab, setPreviewTab] = useState(0);
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const getDarkModeFromRealm = () => {
|
||||
|
|
@ -121,11 +128,23 @@ export const ThemeColors = ({
|
|||
reset();
|
||||
};
|
||||
|
||||
const upload = (values: ThemeRealmRepresentation) => {
|
||||
const upload = async (values: ThemeRealmRepresentation) => {
|
||||
form.setValue("bgimage", values.bgimage);
|
||||
form.setValue("favicon", values.favicon);
|
||||
form.setValue("logo", values.logo);
|
||||
form.setValue("logoWidth", values.logoWidth);
|
||||
form.setValue("logoHeight", values.logoHeight);
|
||||
form.reset(values);
|
||||
|
||||
// Update contexts with data URIs so preview reflects uploaded images
|
||||
if (values.logo) {
|
||||
const logoDataUri = await fileToDataUri(values.logo);
|
||||
contextLogo?.setLogo(logoDataUri);
|
||||
}
|
||||
if (values.bgimage) {
|
||||
const bgDataUri = await fileToDataUri(values.bgimage);
|
||||
contextBackground?.setBackground(bgDataUri);
|
||||
}
|
||||
};
|
||||
|
||||
const convert = (values: Record<string, File | string>) => {
|
||||
|
|
@ -134,6 +153,8 @@ export const ThemeColors = ({
|
|||
...realm,
|
||||
favicon: values.favicon as File,
|
||||
logo: values.logo as File,
|
||||
logoWidth: values.logoWidth as string,
|
||||
logoHeight: values.logoHeight as string,
|
||||
bgimage: values.bgimage as File,
|
||||
fileName: values.name as string,
|
||||
attributes: {
|
||||
|
|
@ -201,8 +222,23 @@ export const ThemeColors = ({
|
|||
onChange={(logo) => contextLogo?.setLogo(logo)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<TextControl
|
||||
name={"logoWidth"}
|
||||
label={t("logoWidth")}
|
||||
placeholder="300px"
|
||||
defaultValue="300px"
|
||||
/>
|
||||
<TextControl
|
||||
name={"logoHeight"}
|
||||
label={t("logoHeight")}
|
||||
placeholder="63px"
|
||||
defaultValue="63px"
|
||||
/>
|
||||
<FormGroup label={t("backgroundImage")}>
|
||||
<ImageUpload name="bgimage" />
|
||||
<ImageUpload
|
||||
name="bgimage"
|
||||
onChange={(bg) => contextBackground?.setBackground(bg)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{mapping.map((m) => (
|
||||
<ColorControl
|
||||
|
|
@ -216,7 +252,24 @@ export const ThemeColors = ({
|
|||
</FormAccess>
|
||||
</FlexItem>
|
||||
<FlexItem grow={{ default: "grow" }} style={{ zIndex: 0 }}>
|
||||
<PreviewWindow cssVars={style?.[theme] || {}} />
|
||||
<Tabs
|
||||
activeKey={previewTab}
|
||||
isBox
|
||||
onSelect={(_, index) => setPreviewTab(index as number)}
|
||||
>
|
||||
<Tab title={t("loginPagePreview")} eventKey={0}>
|
||||
<LoginPreviewWindow
|
||||
cssVars={{
|
||||
...(style?.[theme] || {}),
|
||||
logoWidth: style?.["logoWidth"],
|
||||
logoHeight: style?.["logoHeight"],
|
||||
}}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title={t("adminConsolePreview")} eventKey={1}>
|
||||
<PreviewWindow cssVars={style?.[theme] || {}} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<FixedButtonsGroup
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import { useEnvironment } from "@keycloak/keycloak-ui-shared";
|
||||
import { Environment } from "../../environment";
|
||||
import { Tab, TabTitleText } from "@patternfly/react-core";
|
||||
import JSZip from "jszip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -8,7 +9,6 @@ import {
|
|||
useRoutableTab,
|
||||
} from "../../components/routable-tabs/RoutableTabs";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { joinPath } from "../../utils/joinPath";
|
||||
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";
|
||||
import { ThemesTabType, toThemesTab } from "../routes/ThemesTab";
|
||||
import { LogoContext } from "./LogoContext";
|
||||
|
|
@ -24,13 +24,15 @@ export type ThemeRealmRepresentation = RealmRepresentation & {
|
|||
fileName?: string;
|
||||
favicon?: File;
|
||||
logo?: File;
|
||||
logoWidth?: string;
|
||||
logoHeight?: string;
|
||||
bgimage?: File;
|
||||
};
|
||||
|
||||
export default function ThemesTab({ realm, save }: ThemesTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { realm: realmName } = useRealm();
|
||||
const { environment } = useEnvironment();
|
||||
const { environment } = useEnvironment<Environment>();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
|
||||
const saveTheme = async (realm: ThemeRealmRepresentation) => {
|
||||
|
|
@ -84,7 +86,7 @@ styles=css/theme-styles.css
|
|||
parent=keycloak.v2
|
||||
import=common/quick-theme
|
||||
|
||||
styles=css/login.css css/theme-styles.css
|
||||
styles=css/styles.css css/theme-styles.css
|
||||
`,
|
||||
);
|
||||
|
||||
|
|
@ -117,18 +119,20 @@ styles=css/login.css css/theme-styles.css
|
|||
.map(([key, value]) => `--pf-v5-global--${key}: ${value};`)
|
||||
.join("\n");
|
||||
|
||||
const logoCss = (
|
||||
await fetch(joinPath(environment.resourceUrl, "/theme/login.css"))
|
||||
const loginCss = (
|
||||
await fetch(
|
||||
`/resources/${environment.resourceVersion}/login/keycloak.v2/css/styles.css`,
|
||||
)
|
||||
).text();
|
||||
zip.file("theme/quick-theme/common/resources/css/login.css", logoCss);
|
||||
zip.file("theme/quick-theme/common/resources/css/styles.css", loginCss);
|
||||
|
||||
zip.file(
|
||||
"theme/quick-theme/common/resources/css/theme-styles.css",
|
||||
`:root {
|
||||
--keycloak-bg-logo-url: url('../${bgimageName}');
|
||||
--keycloak-logo-url: url('../${logoName}');
|
||||
--keycloak-logo-height: 63px;
|
||||
--keycloak-logo-width: 300px;
|
||||
${bgimage ? `--keycloak-bg-logo-url: url('../${bgimageName}');` : ""}
|
||||
${logo ? `--keycloak-logo-url: url('../${logoName}');` : ""}
|
||||
--keycloak-logo-height: ${realm.logoHeight};
|
||||
--keycloak-logo-width: ${realm.logoWidth};
|
||||
${toCss(styles.light)}
|
||||
}
|
||||
.pf-v5-theme-dark {
|
||||
|
|
|
|||
8
js/apps/admin-ui/src/realm-settings/themes/fileUtils.ts
Normal file
8
js/apps/admin-ui/src/realm-settings/themes/fileUtils.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const fileToDataUri = (file: File) =>
|
||||
new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
resolve(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
|
@ -38,6 +38,7 @@ div.kc-logo-text {
|
|||
width: var(--keycloak-logo-width);
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue