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:
parent
ab41f270fc
commit
5242f5fcb6
5 changed files with 176 additions and 336 deletions
|
@ -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() {
|
||||
|
|
|
@ -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,6 +193,7 @@ export default function MappingDetails() {
|
|||
}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<FormProvider {...form}>
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
|
@ -210,37 +204,21 @@ export default function MappingDetails() {
|
|||
type="text"
|
||||
id="mapperType"
|
||||
name="mapperType"
|
||||
isReadOnly
|
||||
readOnlyVariant="default"
|
||||
value={mapping?.name}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
<TextControl
|
||||
name="name"
|
||||
label={t("name")}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t("mapperNameHelp")} fieldLabelId="name" />
|
||||
}
|
||||
fieldId="name"
|
||||
isRequired
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("required")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
id="name"
|
||||
isReadOnly={isUpdating}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
{...register("name", { required: true })}
|
||||
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")}
|
||||
|
@ -253,6 +231,7 @@ export default function MappingDetails() {
|
|||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,36 +72,24 @@ 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);
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</FormGroup>
|
||||
{isDynamicScopesEnabled && (
|
||||
<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);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{isDynamicScopesEnabled && (
|
||||
<>
|
||||
<DefaultSwitchControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.is.dynamic.scope",
|
||||
|
@ -124,7 +97,10 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
|||
label={t("dynamicScope")}
|
||||
labelIcon={t("dynamicScopeHelp")}
|
||||
onChange={(value) => {
|
||||
setDynamicRegex(value ? form.getValues("name") || "" : "", value);
|
||||
setDynamicRegex(
|
||||
value ? form.getValues("name") || "" : "",
|
||||
value,
|
||||
);
|
||||
}}
|
||||
stringify
|
||||
/>
|
||||
|
@ -138,202 +114,80 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
|||
isDisabled
|
||||
/>
|
||||
)}
|
||||
</FormProvider>
|
||||
</>
|
||||
)}
|
||||
<FormGroup
|
||||
<TextControl
|
||||
name="description"
|
||||
label={t("description")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("scopeDescriptionHelp")}
|
||||
fieldLabelId="description"
|
||||
labelIcon={t("scopeDescriptionHelp")}
|
||||
rules={{
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: t("maxLength"),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
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
|
||||
<SelectControl
|
||||
name="type"
|
||||
defaultValue={allClientScopeTypes[0]}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label={t("type")}
|
||||
toggleId="kc-type"
|
||||
labelIcon={t("scopeTypeHelp")}
|
||||
variant={SelectVariant.single}
|
||||
isOpen={openType}
|
||||
selections={field.value}
|
||||
onToggle={setOpenType}
|
||||
onSelect={(_, value) => {
|
||||
field.onChange(value);
|
||||
setOpenType(false);
|
||||
}}
|
||||
>
|
||||
{clientScopeTypesSelectOptions(t, allClientScopeTypes)}
|
||||
</Select>
|
||||
)}
|
||||
controller={{ defaultValue: allClientScopeTypes[0] }}
|
||||
options={allClientScopeTypes.map((key) => ({
|
||||
key,
|
||||
value: t(`clientScopeType.${key}`),
|
||||
}))}
|
||||
/>
|
||||
</FormGroup>
|
||||
{!clientScope && (
|
||||
<FormGroup
|
||||
label={t("protocol")}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t("protocolHelp")} fieldLabelId="protocol" />
|
||||
}
|
||||
fieldId="kc-protocol"
|
||||
>
|
||||
<Controller
|
||||
<SelectControl
|
||||
name="protocol"
|
||||
defaultValue={providers[0]}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label={t("protocol")}
|
||||
toggleId="kc-protocol"
|
||||
onToggle={isOpen}
|
||||
onSelect={(_, value) => {
|
||||
field.onChange(value);
|
||||
isOpen(false);
|
||||
}}
|
||||
selections={field.value}
|
||||
labelIcon={t("protocolHelp")}
|
||||
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>
|
||||
)}
|
||||
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>(
|
||||
<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"
|
||||
label={t("guiOrder")}
|
||||
labelIcon={t("guiOrderHelp")}
|
||||
type="number"
|
||||
value={field.value}
|
||||
min={0}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
@ -351,6 +205,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
|||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormProvider>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -32,6 +32,7 @@ export const SwitchControl = <
|
|||
const { control } = useFormContext();
|
||||
return (
|
||||
<FormLabel
|
||||
hasNoPaddingTop
|
||||
name={props.name}
|
||||
isRequired={props.rules?.required === true}
|
||||
label={props.label}
|
||||
|
|
Loading…
Reference in a new issue