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:
jenny-s51 2020-12-04 15:37:29 -05:00
parent c2b94c1ed2
commit a7fd36369e
6 changed files with 105 additions and 231 deletions

View file

@ -22,6 +22,8 @@ export const ClientsSection = () => {
const history = useHistory(); const history = useHistory();
const { url } = useRouteMatch(); const { url } = useRouteMatch();
// console.log(form.errors)
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const baseUrl = getBaseUrl(adminClient); const baseUrl = getBaseUrl(adminClient);

View file

@ -38,6 +38,8 @@ export const NewClientForm = () => {
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const methods = useForm<ClientRepresentation>({ defaultValues: client }); const methods = useForm<ClientRepresentation>({ defaultValues: client });
console.log(methods.errors.description);
const save = async () => { const save = async () => {
try { try {
await adminClient.clients.create({ ...client }); await adminClient.clients.create({ ...client });

View file

@ -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>
</>
);
};

View file

@ -8,14 +8,14 @@ import {
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; 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 { FormAccess } from "../components/form-access/FormAccess";
import { RoleFormType } from "./RealmRoleTabs";
export type RealmRoleFormProps = { export type RealmRoleFormProps = {
form: UseFormMethods; form: UseFormMethods<RoleFormType>;
save: SubmitHandler<RoleRepresentation>; save: (role: RoleFormType) => void;
editMode: boolean; editMode: boolean;
reset: () => void; reset: () => void;
}; };
@ -27,7 +27,6 @@ export const RealmRoleForm = ({
reset, reset,
}: RealmRoleFormProps) => { }: RealmRoleFormProps) => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
return ( return (
<FormAccess <FormAccess
isHorizontal isHorizontal

View file

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

View file

@ -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 { 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 { import {
TableComposable, TableComposable,
Tbody, Tbody,
@ -13,34 +11,39 @@ import {
Tr, Tr,
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import { RoleFormType } from "./RealmRoleTabs";
import "./RealmRolesSection.css";
export type KeyValueType = { key: string; value: string }; export type KeyValueType = { key: string; value: string };
type RoleAttributesProps = { type RoleAttributesProps = {
form: UseFormMethods; form: UseFormMethods<RoleFormType>;
save: (role: RoleRepresentation) => void; 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 },
save,
array: { fields, append, remove },
}: RoleAttributesProps) => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "attributes",
});
const columns = ["Key", "Value"]; const columns = ["Key", "Value"];
const onAdd = () => {
append({ key: "", value: "" });
};
return ( return (
<> <>
<FormAccess role="manage-realm" onSubmit={form.handleSubmit(save)}> <FormAccess role="manage-realm" onSubmit={handleSubmit(save)}>
<TableComposable <TableComposable
className="kc-role-attributes__table" className="kc-role-attributes__table"
aria-label="Role attribute keys and values" aria-label="Role attribute keys and values"
@ -67,9 +70,14 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
> >
<TextInput <TextInput
name={`attributes[${rowIndex}].key`} name={`attributes[${rowIndex}].key`}
ref={form.register({ required: true })} ref={register({ required: true })}
aria-label="key-input" aria-label="key-input"
defaultValue={attribute.key} defaultValue={attribute.key}
validated={
errors.attributes && errors.attributes[rowIndex]
? "error"
: "default"
}
/> />
</Td> </Td>
<Td <Td
@ -79,9 +87,10 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
> >
<TextInput <TextInput
name={`attributes[${rowIndex}].value`} name={`attributes[${rowIndex}].value`}
ref={form.register({})} ref={register()}
aria-label="value-input" aria-label="value-input"
defaultValue={attribute.value} defaultValue={attribute.value}
validated={errors.description ? "error" : "default"}
/> />
</Td> </Td>
{rowIndex !== fields.length - 1 && fields.length - 1 !== 0 && ( {rowIndex !== fields.length - 1 && fields.length - 1 !== 0 && (
@ -103,14 +112,25 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
)} )}
{rowIndex === fields.length - 1 && ( {rowIndex === fields.length - 1 && (
<Td key="add-button" id="add-button" dataLabel={columns[2]}> <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 <Button
aria-label={t("roles:addAttributeText")} aria-label={t("roles:addAttributeText")}
id="plus-icon" id="plus-icon"
variant="link" variant="link"
className="kc-role-attributes__plus-icon" className="kc-role-attributes__plus-icon"
onClick={onAdd} onClick={() => append({ key: "", value: "" })}
icon={<PlusCircleIcon />} icon={<PlusCircleIcon />}
isDisabled={!form.formState.isValid} isDisabled={!formState.isValid}
/> />
</Td> </Td>
)} )}
@ -122,12 +142,12 @@ export const RoleAttributes = ({ form, save, reset }: RoleAttributesProps) => {
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"
isDisabled={!form.formState.isValid} isDisabled={!formState.isValid}
> >
{t("common:save")} {t("common:save")}
</Button> </Button>
<Button variant="link" onClick={reset}> <Button onClick={() => {}} variant="link">
{t("common:reload")}{" "} {t("common:reload")}
</Button> </Button>
</ActionGroup> </ActionGroup>
</FormAccess> </FormAccess>