Do not show enabled and required for email and username (#3766)

This commit is contained in:
Erik Jan de Wit 2022-11-16 09:19:09 -05:00 committed by GitHub
parent edf79d41af
commit e6c415628f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 312 additions and 252 deletions

View file

@ -92,7 +92,7 @@ describe("User profile tabs", () => {
userProfileTab.cancelRemovingValidator();
userProfileTab.removeValidator();
cy.get('tbody [class="kc-emptyValidators"]').contains("No validators.");
cy.get(".kc-emptyValidators").contains("No validators.");
});
});

View file

@ -14,7 +14,7 @@ export default class UserProfile {
private newAttributeCheckboxes = 'input[type="checkbox"]';
private newAttributeRequiredFor = 'input[name="roles"]';
private newAttributeRequiredWhen = 'input[name="requiredWhen"]';
private newAttributeEmptyValidators = 'tbody [class="kc-emptyValidators"]';
private newAttributeEmptyValidators = ".kc-emptyValidators";
private newAttributeAnnotationKey = 'input[name="annotations[0].key"]';
private newAttributeAnnotationValue = 'input[name="annotations[0].value"]';
private validatorRolesList = 'tbody [data-label="Role name"]';

View file

@ -23,13 +23,30 @@ import { useAlerts } from "../components/alert/Alerts";
import { UserProfileProvider } from "./user-profile/UserProfileContext";
import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import type { AttributeParams } from "./routes/Attribute";
import type { KeyValueType } from "../components/key-value-form/key-value-convert";
import { convertToFormValues } from "../util";
import { flatten } from "flat";
import "./realm-settings-section.css";
type UserProfileAttributeType = UserProfileAttribute & Attribute & Permission;
type IndexedAnnotations = {
key: string;
value: unknown;
};
export type IndexedValidations = {
key: string;
value?: Record<string, unknown>[];
};
type UserProfileAttributeType = Omit<
UserProfileAttribute,
"validations" | "annotations"
> &
Attribute &
Permission & {
validations: IndexedValidations[];
annotations: IndexedAnnotations[];
};
type Attribute = {
roles: string[];
@ -56,6 +73,8 @@ type PermissionEdit = [
}
];
export const USERNAME_EMAIL = ["username", "email"];
const CreateAttributeFormContent = ({
save,
}: {
@ -108,12 +127,6 @@ export default function NewAttributeSettings() {
const [config, setConfig] = useState<UserProfileConfig | null>(null);
const editMode = attributeName ? true : false;
const convert = (obj: Record<string, unknown>[] | undefined) =>
Object.entries(obj || []).map(([key, value]) => ({
key,
value,
}));
useFetch(
() => adminClient.users.getProfile(),
(config) => {
@ -133,15 +146,38 @@ export default function NewAttributeSettings() {
Object.entries(
flatten<any, any>({ permissions, selector, required }, { safe: true })
).map(([key, value]) => form.setValue(key, value));
form.setValue("annotations", convert(annotations));
form.setValue("validations", validations);
form.setValue(
"annotations",
Object.entries(annotations || {}).map(([key, value]) => ({
key,
value,
}))
);
form.setValue(
"validations",
Object.entries(validations || {}).map(([key, value]) => ({
key,
value,
}))
);
form.setValue("isRequired", required !== undefined);
},
[]
);
const save = async (profileConfig: UserProfileAttributeType) => {
const annotations = (profileConfig.annotations! as KeyValueType[]).reduce(
const validations = profileConfig.validations.reduce(
(prevValidations, currentValidations) => {
prevValidations[currentValidations.key] =
currentValidations.value?.length === 0
? {}
: currentValidations.value;
return prevValidations;
},
{} as Record<string, unknown>
);
const annotations = profileConfig.annotations.reduce(
(obj, item) => Object.assign(obj, { [item.key]: item.value }),
{}
);
@ -161,6 +197,7 @@ export default function NewAttributeSettings() {
selector: profileConfig.selector,
permissions: profileConfig.permissions!,
annotations,
validations,
},
profileConfig.isRequired
? { required: profileConfig.required }

View file

@ -1,6 +1,11 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalVariant } from "@patternfly/react-core";
import {
Modal,
ModalVariant,
Text,
TextVariants,
} from "@patternfly/react-core";
import {
TableComposable,
Tbody,
@ -10,13 +15,13 @@ import {
Tr,
} from "@patternfly/react-table";
import type { KeyValueType } from "../../../components/key-value-form/key-value-convert";
import type { IndexedValidations } from "../../NewAttributeSettings";
import { AddValidatorRoleDialog } from "./AddValidatorRoleDialog";
import { Validator, validators as allValidator } from "./Validators";
import useToggle from "../../../utils/useToggle";
export type AddValidatorDialogProps = {
selectedValidators: KeyValueType[];
selectedValidators: IndexedValidations[];
toggleDialog: () => void;
onConfirm: (newValidator: Validator) => void;
};
@ -56,33 +61,39 @@ export const AddValidatorDialog = ({
isOpen
onClose={toggleDialog}
>
<TableComposable aria-label="validators-table">
<Thead>
<Tr>
<Th>{t("validatorDialogColNames.colName")}</Th>
<Th>{t("validatorDialogColNames.colDescription")}</Th>
</Tr>
</Thead>
<Tbody>
{validators.map((validator) => (
<Tr
key={validator.name}
onRowClick={() => {
setSelectedValidator(validator);
toggleModal();
}}
isHoverable
>
<Td dataLabel={t("validatorDialogColNames.colName")}>
{validator.name}
</Td>
<Td dataLabel={t("validatorDialogColNames.colDescription")}>
{validator.description}
</Td>
{validators.length !== 0 ? (
<TableComposable>
<Thead>
<Tr>
<Th>{t("validatorDialogColNames.colName")}</Th>
<Th>{t("validatorDialogColNames.colDescription")}</Th>
</Tr>
))}
</Tbody>
</TableComposable>
</Thead>
<Tbody>
{validators.map((validator) => (
<Tr
key={validator.name}
onRowClick={() => {
setSelectedValidator(validator);
toggleModal();
}}
isHoverable
>
<Td dataLabel={t("validatorDialogColNames.colName")}>
{validator.name}
</Td>
<Td dataLabel={t("validatorDialogColNames.colDescription")}>
{validator.description}
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
) : (
<Text className="kc-emptyValidators" component={TextVariants.h6}>
{t("realm-settings:emptyValidators")}
</Text>
)}
</Modal>
</>
);

View file

@ -21,6 +21,7 @@ import { useParams } from "react-router-dom";
import { isEqual } from "lodash-es";
import "../../realm-settings-section.css";
import { USERNAME_EMAIL } from "../../NewAttributeSettings";
const REQUIRED_FOR = [
{ label: "requiredForLabel.both", value: ["admin", "user"] },
@ -151,187 +152,44 @@ export const AttributeGeneralSettings = () => {
)}
></Controller>
</FormGroup>
<Divider />
<FormGroup label={t("enabledWhen")} fieldId="enabledWhen" hasNoPaddingTop>
<Radio
id="always"
data-testid="always"
isChecked={selectedScopes.length === clientScopes?.length}
name="enabledWhen"
label={t("always")}
onChange={(value) => {
if (value) {
form.setValue(
"selector.scopes",
clientScopes?.map((s) => s.name)
);
} else {
form.setValue("selector.scopes", []);
}
}}
className="pf-u-mb-md"
/>
<Radio
id="scopesAsRequested"
data-testid="scopesAsRequested"
isChecked={selectedScopes.length !== clientScopes?.length}
name="enabledWhen"
label={t("scopesAsRequested")}
onChange={(value) => {
if (value) {
form.setValue("selector.scopes", []);
} else {
form.setValue(
"selector.scopes",
clientScopes?.map((s) => s.name)
);
}
}}
className="pf-u-mb-md"
/>
</FormGroup>
<FormGroup fieldId="kc-scope-enabled-when">
<Controller
name="selector.scopes"
control={form.control}
defaultValue={[]}
render={({ onChange, value }) => (
<Select
name="scopes"
data-testid="enabled-when-scope-field"
variant={SelectVariant.typeaheadMulti}
typeAheadAriaLabel="Select"
chipGroupProps={{
numChips: 3,
expandedText: t("common:hide"),
collapsedText: t("common:showRemaining"),
}}
onToggle={(isOpen) => setSelectEnabledWhenOpen(isOpen)}
selections={value}
onSelect={(_, selectedValue) => {
const option = selectedValue.toString();
let changedValue = [""];
if (value) {
changedValue = value.includes(option)
? value.filter((item: string) => item !== option)
: [...value, option];
} else {
changedValue = [option];
}
onChange(changedValue);
}}
onClear={(selectedValues) => {
selectedValues.stopPropagation();
onChange([]);
}}
isOpen={selectEnabledWhenOpen}
isDisabled={selectedScopes.length === clientScopes?.length}
aria-labelledby={"scope"}
>
{clientScopes?.map((option) => (
<SelectOption key={option.name} value={option.name} />
))}
</Select>
)}
/>
</FormGroup>
<Divider />
<FormGroup
label={t("required")}
labelIcon={
<HelpItem
helpText="realm-settings-help:requiredHelp"
fieldLabelId="realm-settings:required"
/>
}
fieldId="kc-required"
hasNoPaddingTop
>
<Controller
name="isRequired"
data-testid="required"
defaultValue={false}
control={form.control}
render={({ onChange, value }) => (
<Switch
id={"kc-required"}
onChange={onChange}
isChecked={value}
label={t("common:on")}
labelOff={t("common:off")}
aria-label={t("required")}
/>
)}
/>
</FormGroup>
{required && (
{!USERNAME_EMAIL.includes(attributeName) && (
<>
<Divider />
<FormGroup
label={t("requiredFor")}
fieldId="requiredFor"
hasNoPaddingTop
>
<Controller
name="required.roles"
data-testid="requiredFor"
defaultValue={REQUIRED_FOR[0].value}
control={form.control}
render={({ onChange, value }) => (
<div className="kc-requiredFor">
{REQUIRED_FOR.map((option) => (
<Radio
id={option.label}
key={option.label}
data-testid={option.label}
isChecked={isEqual(value, option.value)}
name="roles"
onChange={() => {
onChange(option.value);
}}
label={t(option.label)}
className="kc-requiredFor-option"
/>
))}
</div>
)}
/>
</FormGroup>
<FormGroup
label={t("requiredWhen")}
fieldId="requiredWhen"
label={t("enabledWhen")}
fieldId="enabledWhen"
hasNoPaddingTop
>
<Radio
id="requiredAlways"
data-testid="requiredAlways"
isChecked={requiredScopes.length === clientScopes?.length}
name="requiredWhen"
id="always"
data-testid="always"
isChecked={selectedScopes.length === clientScopes?.length}
name="enabledWhen"
label={t("always")}
onChange={(value) => {
if (value) {
form.setValue(
"required.scopes",
"selector.scopes",
clientScopes?.map((s) => s.name)
);
} else {
form.setValue("required.scopes", []);
form.setValue("selector.scopes", []);
}
}}
className="pf-u-mb-md"
/>
<Radio
id="requiredScopesAsRequested"
data-testid="requiredScopesAsRequested"
isChecked={requiredScopes.length !== clientScopes?.length}
name="requiredWhen"
id="scopesAsRequested"
data-testid="scopesAsRequested"
isChecked={selectedScopes.length !== clientScopes?.length}
name="enabledWhen"
label={t("scopesAsRequested")}
onChange={(value) => {
if (value) {
form.setValue("required.scopes", []);
form.setValue("selector.scopes", []);
} else {
form.setValue(
"required.scopes",
"selector.scopes",
clientScopes?.map((s) => s.name)
);
}
@ -339,15 +197,15 @@ export const AttributeGeneralSettings = () => {
className="pf-u-mb-md"
/>
</FormGroup>
<FormGroup fieldId="kc-scope-required-when">
<FormGroup fieldId="kc-scope-enabled-when">
<Controller
name="required.scopes"
name="selector.scopes"
control={form.control}
defaultValue={[]}
render={({ onChange, value }) => (
<Select
name="scopeRequired"
data-testid="required-when-scope-field"
name="scopes"
data-testid="enabled-when-scope-field"
variant={SelectVariant.typeaheadMulti}
typeAheadAriaLabel="Select"
chipGroupProps={{
@ -355,7 +213,7 @@ export const AttributeGeneralSettings = () => {
expandedText: t("common:hide"),
collapsedText: t("common:showRemaining"),
}}
onToggle={(isOpen) => setSelectRequiredForOpen(isOpen)}
onToggle={(isOpen) => setSelectEnabledWhenOpen(isOpen)}
selections={value}
onSelect={(_, selectedValue) => {
const option = selectedValue.toString();
@ -367,14 +225,15 @@ export const AttributeGeneralSettings = () => {
} else {
changedValue = [option];
}
onChange(changedValue);
}}
onClear={(selectedValues) => {
selectedValues.stopPropagation();
onChange([]);
}}
isOpen={selectRequiredForOpen}
isDisabled={requiredScopes.length === clientScopes?.length}
isOpen={selectEnabledWhenOpen}
isDisabled={selectedScopes.length === clientScopes?.length}
aria-labelledby={"scope"}
>
{clientScopes?.map((option) => (
@ -384,6 +243,159 @@ export const AttributeGeneralSettings = () => {
)}
/>
</FormGroup>
<Divider />
<FormGroup
label={t("required")}
labelIcon={
<HelpItem
helpText="realm-settings-help:requiredHelp"
fieldLabelId="realm-settings:required"
/>
}
fieldId="kc-required"
hasNoPaddingTop
>
<Controller
name="isRequired"
data-testid="required"
defaultValue={false}
control={form.control}
render={({ onChange, value }) => (
<Switch
id={"kc-required"}
onChange={onChange}
isChecked={value}
label={t("common:on")}
labelOff={t("common:off")}
aria-label={t("required")}
/>
)}
/>
</FormGroup>
{required && (
<>
<FormGroup
label={t("requiredFor")}
fieldId="requiredFor"
hasNoPaddingTop
>
<Controller
name="required.roles"
data-testid="requiredFor"
defaultValue={REQUIRED_FOR[0].value}
control={form.control}
render={({ onChange, value }) => (
<div className="kc-requiredFor">
{REQUIRED_FOR.map((option) => (
<Radio
id={option.label}
key={option.label}
data-testid={option.label}
isChecked={isEqual(value, option.value)}
name="roles"
onChange={() => {
onChange(option.value);
}}
label={t(option.label)}
className="kc-requiredFor-option"
/>
))}
</div>
)}
/>
</FormGroup>
<FormGroup
label={t("requiredWhen")}
fieldId="requiredWhen"
hasNoPaddingTop
>
<Radio
id="requiredAlways"
data-testid="requiredAlways"
isChecked={requiredScopes.length === clientScopes?.length}
name="requiredWhen"
label={t("always")}
onChange={(value) => {
if (value) {
form.setValue(
"required.scopes",
clientScopes?.map((s) => s.name)
);
} else {
form.setValue("required.scopes", []);
}
}}
className="pf-u-mb-md"
/>
<Radio
id="requiredScopesAsRequested"
data-testid="requiredScopesAsRequested"
isChecked={requiredScopes.length !== clientScopes?.length}
name="requiredWhen"
label={t("scopesAsRequested")}
onChange={(value) => {
if (value) {
form.setValue("required.scopes", []);
} else {
form.setValue(
"required.scopes",
clientScopes?.map((s) => s.name)
);
}
}}
className="pf-u-mb-md"
/>
</FormGroup>
<FormGroup fieldId="kc-scope-required-when">
<Controller
name="required.scopes"
control={form.control}
defaultValue={[]}
render={({ onChange, value }) => (
<Select
name="scopeRequired"
data-testid="required-when-scope-field"
variant={SelectVariant.typeaheadMulti}
typeAheadAriaLabel="Select"
chipGroupProps={{
numChips: 3,
expandedText: t("common:hide"),
collapsedText: t("common:showRemaining"),
}}
onToggle={(isOpen) => setSelectRequiredForOpen(isOpen)}
selections={value}
onSelect={(_, selectedValue) => {
const option = selectedValue.toString();
let changedValue = [""];
if (value) {
changedValue = value.includes(option)
? value.filter((item: string) => item !== option)
: [...value, option];
} else {
changedValue = [option];
}
onChange(changedValue);
}}
onClear={(selectedValues) => {
selectedValues.stopPropagation();
onChange([]);
}}
isOpen={selectRequiredForOpen}
isDisabled={
requiredScopes.length === clientScopes?.length
}
aria-labelledby={"scope"}
>
{clientScopes?.map((option) => (
<SelectOption key={option.name} value={option.name} />
))}
</Select>
)}
/>
</FormGroup>
</>
)}
</>
)}
</FormAccess>

View file

@ -7,7 +7,6 @@ import {
TextVariants,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import "../../realm-settings-section.css";
import { PlusCircleIcon } from "@patternfly/react-icons";
import { AddValidatorDialog } from "../attribute/AddValidatorDialog";
import {
@ -18,12 +17,12 @@ import {
Thead,
Tr,
} from "@patternfly/react-table";
import type { IndexedValidations } from "../../NewAttributeSettings";
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
import useToggle from "../../../utils/useToggle";
import { useFormContext, useWatch } from "react-hook-form";
import type { KeyValueType } from "../../../components/key-value-form/key-value-convert";
import "../../realm-settings-section.css";
export const AttributeValidations = () => {
@ -34,7 +33,7 @@ export const AttributeValidations = () => {
}>();
const { setValue, control, register } = useFormContext();
const validators = useWatch<KeyValueType[]>({
const validators = useWatch<IndexedValidations[]>({
name: "validations",
control,
defaultValue: [],
@ -87,47 +86,48 @@ export const AttributeValidations = () => {
{t("realm-settings:addValidator")}
</Button>
<Divider />
<TableComposable aria-label="validators-table">
<Thead>
<Tr>
<Th>{t("validatorColNames.colName")}</Th>
<Th>{t("validatorColNames.colConfig")}</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{validators.map((validator) => (
<Tr key={validator.key}>
<Td dataLabel={t("validatorColNames.colName")}>
{validator.key}
</Td>
<Td dataLabel={t("validatorColNames.colConfig")}>
{JSON.stringify(validator.value)}
</Td>
<Td className="kc--attributes-validations--action-cell">
<Button
key="validator"
variant="link"
data-testid="deleteValidator"
onClick={() => {
toggleDeleteDialog();
setValidatorToDelete({
name: validator.key,
});
}}
>
{t("common:delete")}
</Button>
</Td>
{validators.length !== 0 ? (
<TableComposable>
<Thead>
<Tr>
<Th>{t("validatorColNames.colName")}</Th>
<Th>{t("validatorColNames.colConfig")}</Th>
<Th />
</Tr>
))}
{validators.length === 0 && (
<Text className="kc-emptyValidators" component={TextVariants.h6}>
{t("realm-settings:emptyValidators")}
</Text>
)}
</Tbody>
</TableComposable>
</Thead>
<Tbody>
{validators.map((validator) => (
<Tr key={validator.key}>
<Td dataLabel={t("validatorColNames.colName")}>
{validator.key}
</Td>
<Td dataLabel={t("validatorColNames.colConfig")}>
{JSON.stringify(validator.value)}
</Td>
<Td className="kc--attributes-validations--action-cell">
<Button
key="validator"
variant="link"
data-testid="deleteValidator"
onClick={() => {
toggleDeleteDialog();
setValidatorToDelete({
name: validator.key,
});
}}
>
{t("common:delete")}
</Button>
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
) : (
<Text className="kc-emptyValidators" component={TextVariants.h6}>
{t("realm-settings:emptyValidators")}
</Text>
)}
</div>
</>
);