Merge pull request #331 from jenny-s51/erikBlankFields

Realm roles(role attributes): adds initial empty field on load, also adds empty field on save
This commit is contained in:
mfrances17 2021-01-28 16:24:16 -05:00 committed by GitHub
commit a73b2d3a4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 90 deletions

View file

@ -8,14 +8,14 @@ import {
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { SubmitHandler, UseFormMethods } from "react-hook-form";
import { UseFormMethods } from "react-hook-form";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { FormAccess } from "../components/form-access/FormAccess";
import { RoleFormType } from "./RealmRoleTabs";
export type RealmRoleFormProps = {
form: UseFormMethods;
save: SubmitHandler<RoleRepresentation>;
form: UseFormMethods<RoleFormType>;
save: (role: RoleFormType) => void;
editMode: boolean;
reset: () => void;
};
@ -27,7 +27,6 @@ export const RealmRoleForm = ({
reset,
}: RealmRoleFormProps) => {
const { t } = useTranslation("roles");
return (
<FormAccess
isHorizontal
@ -81,7 +80,7 @@ export const RealmRoleForm = ({
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button onClick={reset} variant="link">
<Button onClick={() => reset()} variant="link">
{editMode ? t("common:reload") : t("common:cancel")}
</Button>
</ActionGroup>

View file

@ -9,7 +9,7 @@ import {
TabTitleText,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient } from "../context/auth/AdminClient";
@ -30,81 +30,78 @@ const arrayToAttributes = (attributeArray: KeyValueType[]) => {
}, initValue);
};
const attributesToArray = (attributes: { [key: string]: string }): any => {
const attributesToArray = (attributes?: {
[key: string]: string[];
}): KeyValueType[] => {
if (!attributes || Object.keys(attributes).length == 0) {
return [
{
key: "",
value: "",
},
];
return [];
}
return Object.keys(attributes).map((key) => ({
key: key,
value: attributes[key],
value: attributes[key][0],
}));
};
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
attributes: KeyValueType[];
};
export const RealmRoleTabs = () => {
const { t } = useTranslation("roles");
const form = useForm<RoleRepresentation>({ mode: "onChange" });
const form = useForm<RoleFormType>({ mode: "onChange" });
const history = useHistory();
const [name, setName] = useState("");
const adminClient = useAdminClient();
const { realm } = useRealm();
const [role, setRole] = useState<RoleRepresentation>();
const [role, setRole] = useState<RoleFormType>();
const { id } = useParams<{ id: string }>();
const { addAlert } = useAlerts();
const [open, setOpen] = useState(false);
const convert = (role: RoleRepresentation) => {
const { attributes, ...rest } = role;
return {
attributes: attributesToArray(attributes),
...rest,
};
};
useEffect(() => {
(async () => {
if (id) {
const fetchedRole = await adminClient.roles.findOneById({ id });
setName(fetchedRole.name!);
setupForm(fetchedRole);
setRole(fetchedRole);
} else {
setName(t("createRole"));
const convertedRole = convert(fetchedRole);
Object.entries(convertedRole).map((entry) => {
form.setValue(entry[0], entry[1]);
});
setRole(convertedRole);
}
})();
}, []);
const setupForm = (role: RoleRepresentation) => {
Object.entries(role).map((entry) => {
if (entry[0] === "attributes") {
form.setValue(entry[0], attributesToArray(entry[1]));
} else {
form.setValue(entry[0], entry[1]);
}
});
};
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "attributes",
});
// reset form to default values
const reset = () => {
setupForm(role!);
};
useEffect(() => append({ key: "", value: "" }), [append, role]);
const save = async (role: RoleRepresentation) => {
const save = async (role: RoleFormType) => {
try {
const { attributes, ...rest } = role;
const roleRepresentation: RoleRepresentation = rest;
if (id) {
if (role.attributes) {
// react-hook-form will use `KeyValueType[]` here we convert it back into an indexed property of string[]
role.attributes = arrayToAttributes(
(role.attributes as unknown) as KeyValueType[]
);
if (attributes) {
roleRepresentation.attributes = arrayToAttributes(attributes);
}
setRole(role!);
setupForm(role!);
await adminClient.roles.updateById({ id }, role);
await adminClient.roles.updateById({ id }, roleRepresentation);
setRole(role);
} else {
await adminClient.roles.create(role);
await adminClient.roles.create(roleRepresentation);
const createdRole = await adminClient.roles.findOneByName({
name: role.name!,
});
setRole(convert(createdRole));
history.push(`/${realm}/roles/${createdRole.id}`);
}
addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
@ -120,7 +117,9 @@ export const RealmRoleTabs = () => {
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", { name }),
messageKey: t("roles:roleDeleteConfirmDialog", {
name: role?.name || t("createRole"),
}),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
@ -141,7 +140,7 @@ export const RealmRoleTabs = () => {
<DeleteConfirm />
<AssociatedRolesModal open={open} toggleDialog={() => setOpen(!open)} />
<ViewHeader
titleKey={name}
titleKey={role?.name || t("createRole")}
subKey={id ? "" : "roles:roleCreateExplain"}
dropdownItems={
id
@ -172,7 +171,7 @@ export const RealmRoleTabs = () => {
title={<TabTitleText>{t("details")}</TabTitleText>}
>
<RealmRoleForm
reset={reset}
reset={() => form.reset(role)}
form={form}
save={save}
editMode={true}
@ -182,13 +181,18 @@ export const RealmRoleTabs = () => {
eventKey="attributes"
title={<TabTitleText>{t("attributes")}</TabTitleText>}
>
<RoleAttributes form={form} save={save} reset={reset} />
<RoleAttributes
form={form}
save={save}
array={{ fields, append, remove }}
reset={() => form.reset(role)}
/>
</Tab>
</KeycloakTabs>
)}
{!id && (
<RealmRoleForm
reset={reset}
reset={() => form.reset()}
form={form}
save={save}
editMode={false}

View file

@ -7,8 +7,7 @@ import {
ButtonVariant,
PageSection,
} from "@patternfly/react-core";
import { IFormatter, IFormatterValueType } from "@patternfly/react-table";
import { boolFormatter } from "../util";
import { useAdminClient } from "../context/auth/AdminClient";
import { ViewHeader } from "../components/view-header/ViewHeader";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
@ -17,7 +16,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
import { formattedLinkTableCell } from "../components/external-link/FormattedLink";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter, toUpperCase } from "../util";
import { emptyFormatter } from "../util";
export const RealmRolesSection = () => {
const { t } = useTranslation("roles");
@ -45,12 +44,6 @@ export const RealmRolesSection = () => {
</>
);
const boolFormatter = (): IFormatter => (data?: IFormatterValueType) => {
const boolVal = data?.toString();
return (boolVal ? toUpperCase(boolVal) : undefined) as string;
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", {

View file

@ -1,9 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ArrayField, UseFormMethods } from "react-hook-form";
import { ActionGroup, Button, TextInput } from "@patternfly/react-core";
import { useFieldArray, UseFormMethods } from "react-hook-form";
import "./RealmRolesSection.css";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import {
TableComposable,
Tbody,
@ -13,34 +11,42 @@ import {
Tr,
} from "@patternfly/react-table";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../components/form-access/FormAccess";
import { RoleFormType } from "./RealmRoleTabs";
import "./RealmRolesSection.css";
export type KeyValueType = { key: string; value: string };
type RoleAttributesProps = {
form: UseFormMethods;
save: (role: RoleRepresentation) => void;
form: UseFormMethods<RoleFormType>;
save: (role: RoleFormType) => void;
reset: () => void;
array: {
fields: Partial<ArrayField<Record<string, any>, "id">>[];
append: (
value: Partial<Record<string, any>> | Partial<Record<string, any>>[],
shouldFocus?: boolean | undefined
) => void;
remove: (index?: number | number[] | undefined) => void;
};
};
export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
export const RoleAttributes = ({
form: { handleSubmit, register, formState, errors, watch },
save,
array: { fields, append, remove },
reset,
}: RoleAttributesProps) => {
const { t } = useTranslation("roles");
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "attributes",
});
const columns = ["Key", "Value"];
const onAdd = () => {
append({ key: "", value: "" });
};
const watchFirstKey = watch("attributes[0].key");
return (
<>
<FormAccess role="manage-realm" onSubmit={form.handleSubmit(save)}>
<FormAccess role="manage-realm" onSubmit={handleSubmit(save)}>
<TableComposable
className="kc-role-attributes__table"
aria-label="Role attribute keys and values"
@ -67,9 +73,14 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
>
<TextInput
name={`attributes[${rowIndex}].key`}
ref={form.register({ required: true })}
ref={register({ required: true })}
aria-label="key-input"
defaultValue={attribute.key}
validated={
errors.attributes && errors.attributes[rowIndex]
? "error"
: "default"
}
/>
</Td>
<Td
@ -79,9 +90,10 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
>
<TextInput
name={`attributes[${rowIndex}].value`}
ref={form.register({})}
ref={register()}
aria-label="value-input"
defaultValue={attribute.value}
validated={errors.description ? "error" : "default"}
/>
</Td>
{rowIndex !== fields.length - 1 && fields.length - 1 !== 0 && (
@ -103,14 +115,25 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
)}
{rowIndex === fields.length - 1 && (
<Td key="add-button" id="add-button" dataLabel={columns[2]}>
{fields[rowIndex].key === "" && (
<Button
id={`minus-button-${rowIndex}`}
aria-label={`remove ${attribute.key} with value ${attribute.value} `}
variant="link"
className="kc-role-attributes__minus-icon"
onClick={() => remove(rowIndex)}
>
<MinusCircleIcon />
</Button>
)}
<Button
aria-label={t("roles:addAttributeText")}
id="plus-icon"
variant="link"
className="kc-role-attributes__plus-icon"
onClick={onAdd}
onClick={() => append({ key: "", value: "" })}
icon={<PlusCircleIcon />}
isDisabled={!form.formState.isValid}
isDisabled={!formState.isValid}
/>
</Td>
)}
@ -119,15 +142,11 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
</Tbody>
</TableComposable>
<ActionGroup className="kc-role-attributes__action-group">
<Button
variant="primary"
type="submit"
isDisabled={!form.formState.isValid}
>
<Button variant="primary" type="submit" isDisabled={!watchFirstKey}>
{t("common:save")}
</Button>
<Button variant="link" onClick={reset}>
{t("common:reload")}{" "}
<Button onClick={reset} variant="link">
{t("common:reload")}
</Button>
</ActionGroup>
</FormAccess>