Realm settings(Email): Adds email tab to realm settings section (#549)

* email tab wip

* save username and pw info on auth toggle

* add email tab

* remove comments

* remove log stmt

* add help text

* adjust styles

* format

* fix conflicts and address PR feedback

* add back ref on reply to display name

* rebase and fix conflicts

* prevent save without sender email

* add className prop to formpanel
This commit is contained in:
Jenny 2021-05-03 16:00:12 -04:00 committed by GitHub
parent 78f843cdcc
commit 15677b6bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 603 additions and 230 deletions

View file

@ -8,12 +8,6 @@ describe("Realm settings test", () => {
const sidebarPage = new SidebarPage(); const sidebarPage = new SidebarPage();
const realmSettingsPage = new RealmSettingsPage(); const realmSettingsPage = new RealmSettingsPage();
const managedAccessSwitch = "user-managed-access-switch";
const userRegSwitch = "user-reg-switch";
const forgotPwdSwitch = "forgot-pw-switch";
const rememberMeSwitch = "remember-me-switch";
const verifyEmailSwitch = "verify-email-switch";
describe("Realm settings", function () { describe("Realm settings", function () {
beforeEach(function () { beforeEach(function () {
keycloakBefore(); keycloakBefore();
@ -22,19 +16,33 @@ describe("Realm settings test", () => {
it("Go to general tab", function () { it("Go to general tab", function () {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
realmSettingsPage.toggleSwitch(managedAccessSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch);
realmSettingsPage.saveGeneral(); realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
realmSettingsPage.toggleSwitch(managedAccessSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.managedAccessSwitch);
realmSettingsPage.saveGeneral(); realmSettingsPage.save(realmSettingsPage.generalSaveBtn);
}); });
it("Go to login tab", function () { it("Go to login tab", function () {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
cy.getId("rs-login-tab").click(); cy.getId("rs-login-tab").click();
realmSettingsPage.toggleSwitch(userRegSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.userRegSwitch);
realmSettingsPage.toggleSwitch(forgotPwdSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.forgotPwdSwitch);
realmSettingsPage.toggleSwitch(rememberMeSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.rememberMeSwitch);
realmSettingsPage.toggleSwitch(verifyEmailSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.verifyEmailSwitch);
});
it("Go to email tab", function () {
sidebarPage.goToRealmSettings();
cy.getId("rs-email-tab").click();
realmSettingsPage.addSenderEmail("example@example.com");
cy.wait(100);
realmSettingsPage.toggleCheck(realmSettingsPage.enableSslCheck);
realmSettingsPage.toggleCheck(realmSettingsPage.enableStartTlsCheck);
realmSettingsPage.save(realmSettingsPage.emailSaveBtn);
}); });
it("Go to themes tab", function () { it("Go to themes tab", function () {
@ -42,7 +50,7 @@ describe("Realm settings test", () => {
cy.getId("rs-themes-tab").click(); cy.getId("rs-themes-tab").click();
realmSettingsPage.selectLoginThemeType("keycloak"); realmSettingsPage.selectLoginThemeType("keycloak");
realmSettingsPage.selectAccountThemeType("keycloak"); realmSettingsPage.selectAccountThemeType("keycloak");
realmSettingsPage.selectAdminThemeType("keycloak.v2"); realmSettingsPage.selectAdminThemeType("base");
realmSettingsPage.selectEmailThemeType("base"); realmSettingsPage.selectEmailThemeType("base");
realmSettingsPage.saveThemes(); realmSettingsPage.saveThemes();

View file

@ -1,33 +1,30 @@
export default class RealmSettingsPage { export default class RealmSettingsPage {
saveBtnGeneral: string; generalSaveBtn = "general-tab-save";
saveBtnThemes: string; themesSaveBtn = "themes-tab-save";
loginTab: string; loginTab = "rs-login-tab";
selectLoginTheme: string; selectLoginTheme = "#kc-login-theme";
loginThemeList: string; loginThemeList = "#kc-login-theme + ul";
selectAccountTheme: string; selectAccountTheme = "#kc-account-theme";
accountThemeList: string; accountThemeList = "#kc-account-theme + ul";
selectAdminTheme: string; selectAdminTheme = "#kc-admin-console-theme";
adminThemeList: string; adminThemeList = "#kc-admin-console-theme + ul";
selectEmailTheme: string; selectEmailTheme = "#kc-email-theme";
emailThemeList: string; emailThemeList = "#kc-email-theme + ul";
selectDefaultLocale: string; selectDefaultLocale = "select-default-locale";
defaultLocaleList: string; defaultLocaleList = "select-default-locale + ul";
emailSaveBtn = "email-tab-save";
constructor() { managedAccessSwitch = "user-managed-access-switch";
this.saveBtnGeneral = "general-tab-save"; userRegSwitch = "user-reg-switch";
this.saveBtnThemes = "themes-tab-save"; forgotPwdSwitch = "forgot-pw-switch";
this.loginTab = "rs-login-tab"; rememberMeSwitch = "remember-me-switch";
this.selectLoginTheme = "#kc-login-theme"; emailAsUsernameSwitch = "email-as-username-switch";
this.loginThemeList = "#kc-login-theme + ul"; loginWithEmailSwitch = "login-with-email-switch";
this.selectAccountTheme = "#kc-account-theme"; duplicateEmailsSwitch = "duplicate-emails-switch";
this.accountThemeList = "#kc-account-theme + ul"; verifyEmailSwitch = "verify-email-switch";
this.selectAdminTheme = "#kc-admin-console-theme"; authSwitch = "email-authentication-switch";
this.adminThemeList = "#kc-admin-console-theme + ul"; fromInput = "sender-email-address";
this.selectEmailTheme = "#kc-email-theme"; enableSslCheck = "enable-ssl";
this.emailThemeList = "#kc-email-theme + ul"; enableStartTlsCheck = "enable-start-tls";
this.selectDefaultLocale = "select-default-locale";
this.defaultLocaleList = "select-default-locale + ul";
}
selectLoginThemeType(themeType: string) { selectLoginThemeType(themeType: string) {
cy.get(this.selectLoginTheme).click(); cy.get(this.selectLoginTheme).click();
@ -59,20 +56,42 @@ export default class RealmSettingsPage {
return this; return this;
} }
saveGeneral() {
cy.getId(this.generalSaveBtn).click();
return this;
}
saveThemes() {
cy.getId(this.themesSaveBtn).click();
return this;
}
addSenderEmail(senderEmail: string) {
cy.getId(this.fromInput).clear();
if (senderEmail) {
cy.getId(this.fromInput).type(senderEmail);
}
return this;
}
toggleSwitch(switchName: string) { toggleSwitch(switchName: string) {
cy.getId(switchName).next().click(); cy.getId(switchName).next().click();
return this; return this;
} }
saveGeneral() { toggleCheck(switchName: string) {
cy.getId(this.saveBtnGeneral).click(); cy.getId(switchName).click();
return this; return this;
} }
saveThemes() { save(saveBtn: string) {
cy.getId(this.saveBtnThemes).click(); cy.getId(saveBtn).click();
return this; return this;
} }

View file

@ -13,12 +13,18 @@ type FormPanelProps = {
title: string; title: string;
scrollId?: string; scrollId?: string;
children: ReactNode; children: ReactNode;
className?: string;
}; };
export const FormPanel = ({ title, children, scrollId }: FormPanelProps) => { export const FormPanel = ({
title,
children,
scrollId,
className,
}: FormPanelProps) => {
return ( return (
<Card isFlat className="kc-form-panel__panel"> <Card className={className} isFlat>
<CardHeader> <CardHeader className="kc-form-panel__header">
<CardTitle tabIndex={0}> <CardTitle tabIndex={0}>
<Title <Title
headingLevel="h4" headingLevel="h4"
@ -31,7 +37,7 @@ export const FormPanel = ({ title, children, scrollId }: FormPanelProps) => {
</Title> </Title>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardBody>{children}</CardBody> <CardBody className="kc-form-panel__body">{children}</CardBody>
</Card> </Card>
); );
}; };

View file

@ -0,0 +1,295 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext, UseFormMethods } from "react-hook-form";
import {
ActionGroup,
Button,
Checkbox,
FormGroup,
PageSection,
Switch,
TextInput,
} from "@patternfly/react-core";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { FormAccess } from "../components/form-access/FormAccess";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormPanel } from "../components/scroll-form/FormPanel";
import "./RealmSettingsSection.css";
import { emailRegexPattern } from "../util";
export type UserFormProps = {
form: UseFormMethods<RealmRepresentation>;
};
type RealmSettingsEmailTabProps = {
save: (realm: RealmRepresentation) => void;
reset: () => void;
};
export const RealmSettingsEmailTab = ({
save,
reset,
}: RealmSettingsEmailTabProps) => {
const { t } = useTranslation("realm-settings");
const [isAuthenticationEnabled, setAuthenticationEnabled] = useState("");
const { register, control, handleSubmit, errors } = useFormContext();
return (
<>
<PageSection variant="light">
<FormPanel title={t("template")} className="kc-email-template">
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={handleSubmit(save)}
>
<FormGroup
label={t("from")}
fieldId="kc-display-name"
isRequired
validated={
errors.attributes?.from?.type === "pattern"
? "error"
: "default"
}
helperTextInvalid={t("users:emailInvalid")}
>
<TextInput
type="email"
id="kc-sender-email-address"
data-testid="sender-email-address"
name="attributes.from"
ref={register({
pattern: emailRegexPattern,
required: true,
})}
placeholder="Sender email address"
/>
</FormGroup>
<FormGroup
label={t("fromDisplayName")}
fieldId="kc-from-display-name"
labelIcon={
<HelpItem
helpText="realm-settings-help:fromDisplayName"
forLabel={t("authentication")}
forID="kc-user-manged-access"
/>
}
>
<TextInput
type="text"
id="kc-from-display-name"
data-testid="from-display-name"
name="attributes.fromDisplayName"
ref={register}
placeholder="Display name for Sender email address"
/>
</FormGroup>
<FormGroup
label={t("replyTo")}
fieldId="kc-reply-to"
validated={
errors.attributes?.replyTo?.type === "pattern"
? "error"
: "default"
}
helperTextInvalid={t("users:emailInvalid")}
>
<TextInput
type="email"
id="kc-reply-to"
name="attributes.replyTo"
ref={register({
pattern: emailRegexPattern,
})}
placeholder="Reply to email address"
/>
</FormGroup>
<FormGroup
label={t("replyToDisplayName")}
fieldId="kc-reply-to-display-name"
labelIcon={
<HelpItem
helpText="realm-settings-help:replyToDisplayName"
forLabel={t("replyToDisplayName")}
forID="kc-user-manged-access"
/>
}
>
<TextInput
type="text"
id="kc-reply-to-display-name"
name="attributes.replyToDisplayName"
ref={register}
placeholder='Display name for "reply to" email address'
/>
</FormGroup>
<FormGroup
label={t("envelopeFrom")}
fieldId="kc-envelope-from"
labelIcon={
<HelpItem
helpText="realm-settings-help:envelopeFrom"
forLabel={t("envelopeFrom")}
forID="kc-envelope-from"
/>
}
>
<TextInput
type="text"
id="kc-envelope-from"
name="attributes.envelopeFrom"
ref={register}
placeholder="Sender envelope email address"
/>
</FormGroup>
</FormAccess>
</FormPanel>
<FormPanel
className="kc-email-connection"
title={t("connectionAndAuthentication")}
>
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={handleSubmit(save)}
>
<FormGroup label={t("host")} fieldId="kc-host" isRequired>
<TextInput
type="text"
id="kc-host"
name="attributes.host"
ref={register({ required: true })}
placeholder="SMTP host"
/>
</FormGroup>
<FormGroup label={t("port")} fieldId="kc-port">
<TextInput
type="text"
id="kc-port"
name="attributes.port"
ref={register}
placeholder="SMTP port (defaults to 25)"
/>
</FormGroup>
<FormGroup label={t("encryption")} fieldId="kc-html-display-name">
<Controller
name="attributes.enableSsl"
control={control}
defaultValue="false"
render={({ onChange, value }) => (
<Checkbox
id="kc-enable-ssl"
data-testid="enable-ssl"
name="attributes.enableSsl"
label={t("enableSSL")}
ref={register}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
<Controller
name="attributes.enableStartTls"
control={control}
defaultValue="false"
render={({ onChange, value }) => (
<Checkbox
id="kc-enable-start-tls"
data-testid="enable-start-tls"
name="attributes.startTls"
label={t("enableStartTLS")}
ref={register}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
</FormGroup>
<FormGroup
hasNoPaddingTop
label={t("authentication")}
fieldId="kc-authentication"
>
<Controller
name="attributes.authentication"
control={control}
defaultValue="true"
render={({ onChange, value }) => (
<Switch
id="kc-authentication"
data-testid="email-authentication-switch"
label={t("common:enabled")}
labelOff={t("common:disabled")}
isChecked={value === "true"}
onChange={(value) => {
onChange("" + value);
setAuthenticationEnabled(String(value));
}}
/>
)}
/>
</FormGroup>
{isAuthenticationEnabled === "true" && (
<>
<FormGroup
label={t("username")}
fieldId="kc-username"
isRequired={isAuthenticationEnabled === "true"}
>
<TextInput
type="text"
id="kc-username"
data-testid="username-input"
name="attributes.loginUsername"
ref={register({ required: true })}
placeholder="Login username"
/>
</FormGroup>
<FormGroup
label={t("password")}
fieldId="kc-username"
isRequired={isAuthenticationEnabled === "true"}
labelIcon={
<HelpItem
helpText="realm-settings-help:frontendUrl"
forLabel={t("password")}
forID="kc-password"
/>
}
>
<TextInput
type="password"
id="kc-password"
data-testid="password-input"
name="attributes.loginPassword"
ref={register}
placeholder="Login password"
/>
</FormGroup>
</>
)}
<ActionGroup>
<Button
variant="primary"
type="submit"
data-testid="email-tab-save"
>
{t("common:save")}
</Button>
<Button variant="link" onClick={reset}>
{t("common:revert")}
</Button>
</ActionGroup>
</FormAccess>
</FormPanel>
</PageSection>
</>
);
};

View file

@ -21,7 +21,6 @@ export const RealmSettingsLoginTab = ({
<> <>
<PageSection variant="light"> <PageSection variant="light">
<FormPanel title="Login screen customization"> <FormPanel title="Login screen customization">
{
<FormAccess isHorizontal role="manage-realm"> <FormAccess isHorizontal role="manage-realm">
<FormGroup <FormGroup
label={t("userRegistration")} label={t("userRegistration")}
@ -96,10 +95,8 @@ export const RealmSettingsLoginTab = ({
/> />
</FormGroup> </FormGroup>
</FormAccess> </FormAccess>
}
</FormPanel> </FormPanel>
<FormPanel title="Email settings"> <FormPanel title="Email settings">
{
<FormAccess isHorizontal role="manage-realm"> <FormAccess isHorizontal role="manage-realm">
<FormGroup <FormGroup
label={t("emailAsUsername")} label={t("emailAsUsername")}
@ -206,7 +203,6 @@ export const RealmSettingsLoginTab = ({
/> />
</FormGroup> </FormGroup>
</FormAccess> </FormAccess>
}
</FormPanel> </FormPanel>
</PageSection> </PageSection>
</> </>

View file

@ -0,0 +1,18 @@
.pf-c-card.pf-m-flat.kc-email-template,
.pf-c-card.pf-m-flat.kc-email-connection {
border: none;
margin-top: 0px;
margin-bottom: 0px;
padding-bottom: var(--pf-global--spacer--sm);
}
div.pf-c-card__header.kc-form-panel__header {
padding-bottom: 0px;
padding-left: 0px;
padding-top: 0px;
}
div.pf-c-card__body.kc-form-panel__body {
padding-left: 0px;
padding-bottom: var(--pf-global--spacer--2xl);
}

View file

@ -25,6 +25,7 @@ import { RealmSettingsLoginTab } from "./LoginTab";
import { RealmSettingsGeneralTab } from "./GeneralTab"; import { RealmSettingsGeneralTab } from "./GeneralTab";
import { PartialImportDialog } from "./PartialImport"; import { PartialImportDialog } from "./PartialImport";
import { RealmSettingsThemesTab } from "./ThemesTab"; import { RealmSettingsThemesTab } from "./ThemesTab";
import { RealmSettingsEmailTab } from "./EmailTab";
type RealmSettingsHeaderProps = { type RealmSettingsHeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -186,6 +187,16 @@ export const RealmSettingsSection = () => {
> >
<RealmSettingsLoginTab save={save} realm={realm!} /> <RealmSettingsLoginTab save={save} realm={realm!} />
</Tab> </Tab>
<Tab
eventKey="email"
title={<TabTitleText>{t("realm-settings:email")}</TabTitleText>}
data-testid="rs-email-tab"
>
<RealmSettingsEmailTab
save={save}
reset={() => setupForm(realm!)}
/>
</Tab>
<Tab <Tab
eventKey="themes" eventKey="themes"
title={<TabTitleText>{t("realm-settings:themes")}</TabTitleText>} title={<TabTitleText>{t("realm-settings:themes")}</TabTitleText>}

View file

@ -1,5 +1,8 @@
{ {
"realm-settings-help": { "realm-settings-help": {
"fromDisplayName": "A user-friendly name for the 'From' address (optional).",
"replyToDisplayName": "A user-friendly name for the 'Reply-To' address (optional).",
"envelopeFrom": "An email address used for bounces (optional).",
"frontendUrl": "Set the frontend URL for the realm. Use in combination with the default hostname provider to override the base URL for frontend requests for a specific realm.", "frontendUrl": "Set the frontend URL for the realm. Use in combination with the default hostname provider to override the base URL for frontend requests for a specific realm.",
"requireSsl": "Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.", "requireSsl": "Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses.",
"userManagedAccess": "If enabled, users are allowed to manage their resources and permissions using the Account Management Console.", "userManagedAccess": "If enabled, users are allowed to manage their resources and permissions using the Account Management Console.",

View file

@ -14,6 +14,22 @@
"general": "General", "general": "General",
"login": "Login", "login": "Login",
"themes": "Themes", "themes": "Themes",
"email": "Email",
"template": "Template",
"connectionAndAuthentication": "Connection & Authentication",
"from": "From",
"fromDisplayName": "From display name",
"replyTo": "Reply to",
"replyToDisplayName": "Reply to display name",
"envelopeFrom": "Envelope from",
"host": "Host",
"port": "Port",
"encryption": "Encryption",
"authentication": "Authentication",
"enableSSL": "Enable SSL",
"enableStartTLS": "Enable StartTLS",
"username": "Username",
"password": "Password",
"userRegistration": "User registration", "userRegistration": "User registration",
"userRegistrationHelpText": "Enable/disable the registration page. A link for registration will show on login page too.", "userRegistrationHelpText": "Enable/disable the registration page. A link for registration will show on login page too.",
"forgotPassword": "Forgot password", "forgotPassword": "Forgot password",

View file

@ -25,6 +25,7 @@ import moment from "moment";
import { JoinGroupDialog } from "./JoinGroupDialog"; import { JoinGroupDialog } from "./JoinGroupDialog";
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation"; import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { emailRegexPattern } from "../util";
export type UserFormProps = { export type UserFormProps = {
form: UseFormMethods<UserRepresentation>; form: UseFormMethods<UserRepresentation>;
@ -86,8 +87,6 @@ export const UserForm = ({
}); });
}; };
const emailRegexPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const requiredUserActionsOptions = [ const requiredUserActionsOptions = [
<SelectOption key={0} value="CONFIGURE_TOTP"> <SelectOption key={0} value="CONFIGURE_TOTP">
{t("configureOTP")} {t("configureOTP")}

View file

@ -91,3 +91,5 @@ export const getBaseUrl = (adminClient: KeycloakAdminClient) => {
? adminClient.keycloak.authServerUrl! ? adminClient.keycloak.authServerUrl!
: adminClient.baseUrl + "/"; : adminClient.baseUrl + "/";
}; };
export const emailRegexPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;