pf5 refactor client scope (#26734)

* use ui-shared controls

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* add `hasNoPaddingTop` to Switch Label

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* use ui-shared controls

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed tests

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-02-13 13:53:38 +01:00 committed by GitHub
parent ab41f270fc
commit 5242f5fcb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 176 additions and 336 deletions

View file

@ -21,18 +21,17 @@ export default class CreateClientScopePage extends CommonPage {
this.settingsTab = ".pf-c-tabs__item:nth-child(1)";
this.mappersTab = ".pf-c-tabs__item:nth-child(2)";
this.clientScopeNameInput = "#kc-name";
this.clientScopeNameError = "#kc-name-helper";
this.clientScopeDescriptionInput = "#kc-description";
this.clientScopeNameInput = "name";
this.clientScopeNameError = "#name-helper";
this.clientScopeDescriptionInput = "description";
this.clientScopeTypeDrpDwn = "#kc-protocol";
this.clientScopeTypeList = "#kc-protocol + ul";
this.displayOnConsentInput = '[id="kc-display-on-consent-screen"]';
this.displayOnConsentInput = "attributes.display🍺on🍺consent🍺screen";
this.displayOnConsentSwitch =
this.displayOnConsentInput + " + .pf-c-switch__toggle";
this.consentScreenTextInput = "#kc-consent-screen-text";
this.includeInTokenSwitch =
'[id="includeInTokenScope"] + .pf-c-switch__toggle';
this.displayOrderInput = "#kc-gui-order";
'[for="attributes.display🍺on🍺consent🍺screen"] .pf-c-switch__toggle';
this.consentScreenTextInput = "attributes.consent🍺screen🍺text";
this.includeInTokenSwitch = "#attributes.include🍺in🍺token🍺scope-on";
this.displayOrderInput = "attributes.gui🍺order";
this.saveBtn = '[type="submit"]';
this.cancelBtn = '[type="button"]';
@ -45,22 +44,22 @@ export default class CreateClientScopePage extends CommonPage {
consentScreenText = "",
displayOrder = "",
) {
cy.get(this.clientScopeNameInput).clear();
cy.findByTestId(this.clientScopeNameInput).clear();
if (name) {
cy.get(this.clientScopeNameInput).type(name);
cy.findByTestId(this.clientScopeNameInput).type(name);
}
if (description) {
cy.get(this.clientScopeDescriptionInput).type(description);
cy.findByTestId(this.clientScopeDescriptionInput).type(description);
}
if (consentScreenText) {
cy.get(this.consentScreenTextInput).type(consentScreenText);
cy.findByTestId(this.consentScreenTextInput).type(consentScreenText);
}
if (displayOrder) {
cy.get(this.displayOrderInput).type(displayOrder);
cy.findByTestId(this.displayOrderInput).type(displayOrder);
}
return this;
@ -74,11 +73,11 @@ export default class CreateClientScopePage extends CommonPage {
}
getSwitchDisplayOnConsentScreenInput() {
return cy.get(this.displayOnConsentInput);
return cy.findByTestId(this.displayOnConsentInput);
}
getConsentScreenTextInput() {
return cy.get(this.consentScreenTextInput);
return cy.findByTestId(this.consentScreenTextInput);
}
switchDisplayOnConsentScreen() {

View file

@ -8,13 +8,12 @@ import {
DropdownItem,
FormGroup,
PageSection,
ValidatedOptions,
} from "@patternfly/react-core";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useMatch, useNavigate } from "react-router-dom";
import { HelpItem } from "ui-shared";
import { KeycloakTextInput, TextControl } from "ui-shared";
import { adminClient } from "../../admin-client";
import { toClient } from "../../clients/routes/Client";
@ -22,7 +21,6 @@ import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { DynamicComponents } from "../../components/dynamic/DynamicComponents";
import { FormAccess } from "../../components/form/FormAccess";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
@ -38,12 +36,7 @@ export default function MappingDetails() {
const { id, mapperId } = useParams<MapperParams>();
const form = useForm();
const {
register,
setValue,
formState: { errors },
handleSubmit,
} = form;
const { setValue, handleSubmit } = form;
const [mapping, setMapping] = useState<ProtocolMapperTypeRepresentation>();
const [config, setConfig] = useState<{
protocol?: string;
@ -200,59 +193,45 @@ export default function MappingDetails() {
}
/>
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<FormGroup label={t("mapperType")} fieldId="mapperType">
<KeycloakTextInput
type="text"
id="mapperType"
name="mapperType"
isReadOnly
value={mapping?.name}
/>
</FormGroup>
<FormGroup
label={t("name")}
labelIcon={
<HelpItem helpText={t("mapperNameHelp")} fieldLabelId="name" />
}
fieldId="name"
isRequired
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("required")}
<FormProvider {...form}>
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<KeycloakTextInput
id="name"
isReadOnly={isUpdating}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
{...register("name", { required: true })}
<FormGroup label={t("mapperType")} fieldId="mapperType">
<KeycloakTextInput
type="text"
id="mapperType"
name="mapperType"
readOnlyVariant="default"
value={mapping?.name}
/>
</FormGroup>
<TextControl
name="name"
label={t("name")}
labelIcon={t("mapperNameHelp")}
readOnlyVariant={isUpdating ? "default" : undefined}
rules={{ required: { value: true, message: t("required") } }}
/>
</FormGroup>
<FormProvider {...form}>
<DynamicComponents
properties={mapping?.properties || []}
isNew={!isUpdating}
/>
</FormProvider>
<ActionGroup>
<Button variant="primary" type="submit">
{t("save")}
</Button>
<Button
variant="link"
component={(props) => <Link {...props} to={toDetails()} />}
>
{t("cancel")}
</Button>
</ActionGroup>
</FormAccess>
<ActionGroup>
<Button variant="primary" type="submit">
{t("save")}
</Button>
<Button
variant="link"
component={(props) => <Link {...props} to={toDetails()} />}
>
{t("cancel")}
</Button>
</ActionGroup>
</FormAccess>
</FormProvider>
</PageSection>
</>
);

View file

@ -1,30 +1,18 @@
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import {
ActionGroup,
Button,
FormGroup,
Select,
SelectOption,
SelectVariant,
Switch,
ValidatedOptions,
} from "@patternfly/react-core";
import { useEffect, useState } from "react";
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
import { ActionGroup, Button, SelectVariant } from "@patternfly/react-core";
import { useEffect } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { HelpItem, TextControl } from "ui-shared";
import { SelectControl, TextAreaControl, TextControl } from "ui-shared";
import { getProtocolName } from "../../clients/utils";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import {
ClientScopeDefaultOptionalType,
allClientScopeTypes,
clientScopeTypesSelectOptions,
} from "../../components/client-scope/ClientScopeTypes";
import { FormAccess } from "../../components/form/FormAccess";
import { KeycloakTextArea } from "../../components/keycloak-text-area/KeycloakTextArea";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { convertAttributeNameToForm, convertToFormValues } from "../../util";
@ -40,19 +28,16 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
const { t } = useTranslation();
const form = useForm<ClientScopeDefaultOptionalType>({ mode: "onChange" });
const {
register,
control,
handleSubmit,
setValue,
formState: { errors, isDirty, isValid },
formState: { isDirty, isValid },
} = form;
const { realm } = useRealm();
const providers = useLoginProviders();
const isFeatureEnabled = useIsFeatureEnabled();
const isDynamicScopesEnabled = isFeatureEnabled(Feature.DynamicScopes);
const [open, isOpen] = useState(false);
const [openType, setOpenType] = useState(false);
const displayOnConsentScreen: string = useWatch({
control,
@ -87,270 +72,140 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
onSubmit={handleSubmit(save)}
isHorizontal
>
<FormGroup
label={t("name")}
labelIcon={
<HelpItem helpText={t("scopeNameHelp")} fieldLabelId="name" />
}
fieldId="kc-name"
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("required")}
isRequired
>
<KeycloakTextInput
id="kc-name"
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
{...register("name", {
required: true,
onChange: (e) => {
if (isDynamicScopesEnabled) {
setDynamicRegex(e.target.value, true);
}
<FormProvider {...form}>
<TextControl
name="name"
label={t("name")}
labelIcon={t("scopeNameHelp")}
rules={{
required: {
value: true,
message: t("required"),
},
})}
onChange: (e) => {
if (isDynamicScopesEnabled)
setDynamicRegex(e.target.validated, true);
},
}}
/>
</FormGroup>
{isDynamicScopesEnabled && (
<FormProvider {...form}>
<DefaultSwitchControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.is.dynamic.scope",
)}
label={t("dynamicScope")}
labelIcon={t("dynamicScopeHelp")}
onChange={(value) => {
setDynamicRegex(value ? form.getValues("name") || "" : "", value);
}}
stringify
/>
{dynamicScope === "true" && (
<TextControl
{isDynamicScopesEnabled && (
<>
<DefaultSwitchControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.dynamic.scope.regexp",
"attributes.is.dynamic.scope",
)}
label={t("dynamicScopeFormat")}
labelIcon={t("dynamicScopeFormatHelp")}
isDisabled
/>
)}
</FormProvider>
)}
<FormGroup
label={t("description")}
labelIcon={
<HelpItem
helpText={t("scopeDescriptionHelp")}
fieldLabelId="description"
/>
}
fieldId="kc-description"
validated={
errors.description ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("maxLength", { length: 255 })}
>
<KeycloakTextInput
id="kc-description"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("description", {
maxLength: 255,
})}
/>
</FormGroup>
<FormGroup
label={t("type")}
labelIcon={
<HelpItem helpText={t("scopeTypeHelp")} fieldLabelId="type" />
}
fieldId="kc-type"
>
<Controller
name="type"
defaultValue={allClientScopeTypes[0]}
control={control}
render={({ field }) => (
<Select
toggleId="kc-type"
variant={SelectVariant.single}
isOpen={openType}
selections={field.value}
onToggle={setOpenType}
onSelect={(_, value) => {
field.onChange(value);
setOpenType(false);
label={t("dynamicScope")}
labelIcon={t("dynamicScopeHelp")}
onChange={(value) => {
setDynamicRegex(
value ? form.getValues("name") || "" : "",
value,
);
}}
>
{clientScopeTypesSelectOptions(t, allClientScopeTypes)}
</Select>
)}
/>
</FormGroup>
{!clientScope && (
<FormGroup
label={t("protocol")}
labelIcon={
<HelpItem helpText={t("protocolHelp")} fieldLabelId="protocol" />
}
fieldId="kc-protocol"
>
<Controller
name="protocol"
defaultValue={providers[0]}
control={control}
render={({ field }) => (
<Select
toggleId="kc-protocol"
onToggle={isOpen}
onSelect={(_, value) => {
field.onChange(value);
isOpen(false);
}}
selections={field.value}
variant={SelectVariant.single}
isOpen={open}
>
{providers.map((option) => (
<SelectOption
selected={option === field.value}
key={option}
value={option}
data-testid={`option-${option}`}
>
{getProtocolName(t, option)}
</SelectOption>
))}
</Select>
stringify
/>
{dynamicScope === "true" && (
<TextControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.dynamic.scope.regexp",
)}
label={t("dynamicScopeFormat")}
labelIcon={t("dynamicScopeFormatHelp")}
isDisabled
/>
)}
</>
)}
<TextControl
name="description"
label={t("description")}
labelIcon={t("scopeDescriptionHelp")}
rules={{
maxLength: {
value: 255,
message: t("maxLength"),
},
}}
/>
<SelectControl
name="type"
label={t("type")}
toggleId="kc-type"
labelIcon={t("scopeTypeHelp")}
variant={SelectVariant.single}
controller={{ defaultValue: allClientScopeTypes[0] }}
options={allClientScopeTypes.map((key) => ({
key,
value: t(`clientScopeType.${key}`),
}))}
/>
{!clientScope && (
<SelectControl
name="protocol"
label={t("protocol")}
toggleId="kc-protocol"
labelIcon={t("protocolHelp")}
variant={SelectVariant.single}
controller={{ defaultValue: providers[0] }}
options={providers.map((option) => ({
key: option,
value: getProtocolName(t, option),
}))}
/>
</FormGroup>
)}
<FormGroup
hasNoPaddingTop
label={t("displayOnConsentScreen")}
labelIcon={
<HelpItem
helpText={t("displayOnConsentScreenHelp")}
fieldLabelId="displayOnConsentScreen"
/>
}
fieldId="kc-display-on-consent-screen"
>
<Controller
)}
<DefaultSwitchControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.display.on.consent.screen",
)}
control={control}
defaultValue={displayOnConsentScreen}
render={({ field }) => (
<Switch
id="kc-display-on-consent-screen"
label={t("on")}
labelOff={t("off")}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
/>
)}
label={t("displayOnConsentScreen")}
labelIcon={t("displayOnConsentScreenHelp")}
stringify
/>
</FormGroup>
{displayOnConsentScreen === "true" && (
<FormGroup
label={t("consentScreenText")}
labelIcon={
<HelpItem
helpText={t("consentScreenTextHelp")}
fieldLabelId="consentScreenText"
/>
}
fieldId="kc-consent-screen-text"
>
<KeycloakTextArea
id="kc-consent-screen-text"
{...register(
convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.consent.screen.text",
),
{displayOnConsentScreen === "true" && (
<TextAreaControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.consent.screen.text",
)}
label={t("consentScreenText")}
labelIcon={t("consentScreenTextHelp")}
/>
</FormGroup>
)}
<FormGroup
hasNoPaddingTop
label={t("includeInTokenScope")}
labelIcon={
<HelpItem
helpText={t("includeInTokenScopeHelp")}
fieldLabelId="includeInTokenScope"
/>
}
fieldId="kc-include-in-token-scope"
>
<Controller
)}
<DefaultSwitchControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.include.in.token.scope",
)}
control={control}
defaultValue="true"
render={({ field }) => (
<Switch
id="kc-include-in-token-scope"
label={t("on")}
labelOff={t("off")}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
/>
)}
label={t("includeInTokenScope")}
labelIcon={t("includeInTokenScopeHelp")}
stringify
/>
</FormGroup>
<FormGroup
label={t("guiOrder")}
labelIcon={
<HelpItem helpText={t("guiOrderHelp")} fieldLabelId="guiOrder" />
}
fieldId="kc-gui-order"
>
<Controller
<TextControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.gui.order",
)}
defaultValue=""
control={control}
render={({ field }) => (
<KeycloakTextInput
id="kc-gui-order"
type="number"
value={field.value}
min={0}
onChange={field.onChange}
/>
)}
label={t("guiOrder")}
labelIcon={t("guiOrderHelp")}
type="number"
min={0}
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
type="submit"
isDisabled={!isDirty || !isValid}
>
{t("save")}
</Button>
<Button
variant="link"
component={(props) => (
<Link {...props} to={toClientScopes({ realm })}></Link>
)}
>
{t("cancel")}
</Button>
</ActionGroup>
<ActionGroup>
<Button
variant="primary"
type="submit"
isDisabled={!isDirty || !isValid}
>
{t("save")}
</Button>
<Button
variant="link"
component={(props) => (
<Link {...props} to={toClientScopes({ realm })}></Link>
)}
>
{t("cancel")}
</Button>
</ActionGroup>
</FormProvider>
</FormAccess>
);
};

View file

@ -1,9 +1,13 @@
import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
import {
FormGroup,
FormGroupProps,
ValidatedOptions,
} from "@patternfly/react-core";
import { PropsWithChildren } from "react";
import { FieldError, FieldValues, Merge } from "react-hook-form";
import { HelpItem } from "./HelpItem";
export type FormLabelProps<T extends FieldValues = FieldValues> = {
export type FieldProps<T extends FieldValues = FieldValues> = {
label?: string;
name: string;
labelIcon?: string;
@ -11,6 +15,8 @@ export type FormLabelProps<T extends FieldValues = FieldValues> = {
isRequired: boolean;
};
type FormLabelProps = FieldProps & Omit<FormGroupProps, "label" | "labelIcon">;
export const FormLabel = ({
name,
label,

View file

@ -32,6 +32,7 @@ export const SwitchControl = <
const { control } = useFormContext();
return (
<FormLabel
hasNoPaddingTop
name={props.name}
isRequired={props.rules?.required === true}
label={props.label}