WIP role attributes
fix storybook demos add attributes tab to realm roles section use TableComposable fix formatting css updates fix up styling of role attributes table fix check-types erros logic from call with Jeff clean up, format, make eslint proud delete roledetails call with Erik add delete function and css changes fix storybook demos and format make key input disabled once new attribute is created minus icon gray address PR feedback from Sarah set add and save buttons to disabled when new input field is empty fix save/add fix onChange formState rebase with resolved conflicts fix paths and use error msg from response WIP readOnly try setting readonly add defaultState prop use keycloak tabs default values WIP reload WIP cleanup function add shouldUnregister fix for adding empty line on form load removed debug small fix for `isValid` role attributes tab format
This commit is contained in:
parent
c2b94c1ed2
commit
a7fd36369e
6 changed files with 105 additions and 231 deletions
|
@ -22,6 +22,8 @@ export const ClientsSection = () => {
|
|||
const history = useHistory();
|
||||
const { url } = useRouteMatch();
|
||||
|
||||
// console.log(form.errors)
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const baseUrl = getBaseUrl(adminClient);
|
||||
|
||||
|
|
|
@ -38,6 +38,8 @@ export const NewClientForm = () => {
|
|||
const { addAlert } = useAlerts();
|
||||
const methods = useForm<ClientRepresentation>({ defaultValues: client });
|
||||
|
||||
console.log(methods.errors.description);
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await adminClient.clients.create({ ...client });
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabTitleText,
|
||||
TextArea,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import { RoleAttributes } from "./RoleAttributes";
|
||||
|
||||
export const RolesForm = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
const { register, handleSubmit, errors, control, setValue } = useForm<
|
||||
RoleRepresentation
|
||||
>();
|
||||
const history = useHistory();
|
||||
const [name, setName] = useState("");
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const form = useForm();
|
||||
|
||||
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const fetchedRole = await adminClient.roles.findOneById({ id });
|
||||
setName(fetchedRole.name!);
|
||||
setupForm(fetchedRole);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const setupForm = (role: RoleRepresentation) => {
|
||||
Object.entries(role).map((entry) => {
|
||||
setValue(entry[0], entry[1]);
|
||||
});
|
||||
};
|
||||
|
||||
const save = async (role: RoleRepresentation) => {
|
||||
try {
|
||||
await adminClient.roles.updateById({ id }, role);
|
||||
setupForm(role as RoleRepresentation);
|
||||
addAlert(t("roleSaveSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(`${t("roleSaveError")} '${error}'`, AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey={name} subKey="" />
|
||||
|
||||
<PageSection variant="light">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(_, key) => setActiveTab(key as number)}
|
||||
isBox
|
||||
>
|
||||
<Tab eventKey={0} title={<TabTitleText>{t("details")}</TabTitleText>}>
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
role="manage-realm"
|
||||
className="pf-u-mt-lg"
|
||||
>
|
||||
<FormGroup
|
||||
label={t("roleName")}
|
||||
fieldId="kc-name"
|
||||
isRequired
|
||||
validated={errors.name ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
{name ? (
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
id="kc-name"
|
||||
name="name"
|
||||
isReadOnly
|
||||
/>
|
||||
) : undefined}
|
||||
</FormGroup>
|
||||
<FormGroup label={t("description")} fieldId="kc-description">
|
||||
<Controller
|
||||
name="description"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
rules={{ maxLength: 255 }}
|
||||
render={({ onChange, value }) => (
|
||||
<TextArea
|
||||
type="text"
|
||||
validated={
|
||||
errors.description
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
id="kc-role-description"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button variant="primary" type="submit">
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={() => history.push("/roles/")}>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</Tab>
|
||||
<Tab eventKey={1} title={<TabTitleText>{t("attributes")}</TabTitleText>}>
|
||||
<RoleAttributes form={form} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -171,29 +170,21 @@ export const RealmRoleTabs = () => {
|
|||
eventKey="details"
|
||||
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||
>
|
||||
<RealmRoleForm
|
||||
reset={reset}
|
||||
form={form}
|
||||
save={save}
|
||||
editMode={true}
|
||||
/>
|
||||
<RealmRoleForm form={form} save={save} editMode={true} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="attributes"
|
||||
title={<TabTitleText>{t("attributes")}</TabTitleText>}
|
||||
>
|
||||
<RoleAttributes form={form} save={save} reset={reset} />
|
||||
<RoleAttributes
|
||||
form={form}
|
||||
save={save}
|
||||
array={{ fields, append, remove }}
|
||||
/>
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && (
|
||||
<RealmRoleForm
|
||||
reset={reset}
|
||||
form={form}
|
||||
save={save}
|
||||
editMode={false}
|
||||
/>
|
||||
)}
|
||||
{!id && <RealmRoleForm form={form} save={save} editMode={false} />}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import React from "react";
|
||||
import React, { useEffect } 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,39 @@ 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;
|
||||
reset: () => void;
|
||||
form: UseFormMethods<RoleFormType>;
|
||||
save: (role: RoleFormType) => 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 },
|
||||
save,
|
||||
array: { fields, append, remove },
|
||||
}: RoleAttributesProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "attributes",
|
||||
});
|
||||
|
||||
const columns = ["Key", "Value"];
|
||||
|
||||
const onAdd = () => {
|
||||
append({ key: "", value: "" });
|
||||
};
|
||||
|
||||
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 +70,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 +87,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 +112,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>
|
||||
)}
|
||||
|
@ -122,12 +142,12 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
|
|||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
isDisabled={!form.formState.isValid}
|
||||
isDisabled={!formState.isValid}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:reload")}{" "}
|
||||
<Button onClick={() => {}} variant="link">
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
|
|
Loading…
Reference in a new issue