merged "new role form" and "role details" into one (#239)

This commit is contained in:
Erik Jan de Wit 2020-12-02 22:04:54 +01:00 committed by GitHub
parent a92f5edcb1
commit 7059297d5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 222 additions and 299 deletions

View file

@ -1,169 +0,0 @@
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
DropdownItem,
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 { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
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 selectedRoleName = name;
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);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", { selectedRoleName }),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.roles.delByName({
name: name,
});
addAlert(t("roleDeletedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger);
}
},
});
return (
<>
<DeleteConfirm />
<ViewHeader
titleKey={name}
subKey=""
dropdownItems={[
<DropdownItem
key="action"
component="button"
onClick={() => toggleDeleteDialog()}
>
{t("deleteRole")}
</DropdownItem>,
]}
/>
<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>
</Tabs>
</PageSection>
</>
);
};

View file

@ -0,0 +1,204 @@
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
DropdownItem,
FormGroup,
PageSection,
Tab,
Tabs,
TabTitleText,
TextArea,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import {
Controller,
SubmitHandler,
useForm,
UseFormMethods,
} from "react-hook-form";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
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 { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
type RoleFormType = {
form: UseFormMethods;
save: SubmitHandler<RoleRepresentation>;
editMode: boolean;
};
const RoleForm = ({ form, save, editMode }: RoleFormType) => {
const { t } = useTranslation("roles");
const history = useHistory();
return (
<FormAccess
isHorizontal
onSubmit={form.handleSubmit(save)}
role="manage-realm"
className="pf-u-mt-lg"
>
<FormGroup
label={t("roleName")}
fieldId="kc-name"
isRequired
validated={form.errors.name ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={form.register({ required: true })}
type="text"
id="kc-name"
name="name"
isReadOnly={editMode}
/>
</FormGroup>
<FormGroup label={t("description")} fieldId="kc-description">
<Controller
name="description"
defaultValue=""
control={form.control}
rules={{ maxLength: 255 }}
render={({ onChange, value }) => (
<TextArea
type="text"
validated={
form.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/")}>
{editMode ? t("common:reload") : t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
);
};
export const RealmRolesForm = () => {
const { t } = useTranslation("roles");
const form = useForm<RoleRepresentation>();
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const history = useHistory();
const { id } = useParams<{ id: string }>();
const [name, setName] = useState("");
const [activeTab, setActiveTab] = useState(0);
useEffect(() => {
(async () => {
if (id) {
const fetchedRole = await adminClient.roles.findOneById({ id });
setName(fetchedRole.name!);
setupForm(fetchedRole);
} else {
setName(t("createRole"));
}
})();
}, []);
const setupForm = (role: RoleRepresentation) => {
Object.entries(role).map((entry) => {
form.setValue(entry[0], entry[1]);
});
};
const save = async (role: RoleRepresentation) => {
try {
if (id) {
await adminClient.roles.updateById({ id }, role);
} else {
await adminClient.roles.create(role);
const createdRole = await adminClient.roles.findOneByName({
name: role.name!,
});
history.push(`/roles/${createdRole.id}`);
}
addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
} catch (error) {
addAlert(
t((id ? "roleSave" : "roleCreate") + "Error", { error }),
AlertVariant.danger
);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", { name }),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.roles.delById({ id });
addAlert(t("roleDeletedSuccess"), AlertVariant.success);
history.push("/roles");
} catch (error) {
addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger);
}
},
});
return (
<>
<DeleteConfirm />
<ViewHeader
titleKey={name}
subKey={id ? "" : "roles:roleCreateExplain"}
dropdownItems={
id
? [
<DropdownItem
key="action"
component="button"
onClick={() => toggleDeleteDialog()}
>
{t("deleteRole")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light">
{id && (
<Tabs
activeKey={activeTab}
onSelect={(_, key) => setActiveTab(key as number)}
isBox
>
<Tab
eventKey={0}
title={<TabTitleText>{t("details")}</TabTitleText>}
>
<RoleForm form={form} save={save} editMode={true} />
</Tab>
</Tabs>
)}
{!id && <RoleForm form={form} save={save} editMode={false} />}
</PageSection>
</>
);
};

View file

@ -25,6 +25,7 @@ export const RealmRolesSection = () => {
loader(); loader();
}, [first, max]); }, [first, max]);
const goToCreate = () => history.push("/roles/add-role");
return ( return (
<> <>
<ViewHeader titleKey="roles:title" subKey="roles:roleExplain" /> <ViewHeader titleKey="roles:title" subKey="roles:roleExplain" />
@ -42,9 +43,7 @@ export const RealmRolesSection = () => {
}} }}
toolbarItem={ toolbarItem={
<> <>
<Button onClick={() => history.push("/add-role")}> <Button onClick={goToCreate}>{t("createRole")}</Button>
{t("createRole")}
</Button>
</> </>
} }
> >
@ -56,7 +55,7 @@ export const RealmRolesSection = () => {
message={t("noRolesInThisRealm")} message={t("noRolesInThisRealm")}
instructions={t("noRolesInThisRealmInstructions")} instructions={t("noRolesInThisRealmInstructions")}
primaryActionText={t("createRole")} primaryActionText={t("createRole")}
onPrimaryAction={() => history.push("/add-role")} onPrimaryAction={goToCreate}
/> />
)} )}
</PageSection> </PageSection>

View file

@ -1,111 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
Text,
PageSection,
TextContent,
FormGroup,
Form,
TextInput,
ActionGroup,
Button,
Divider,
AlertVariant,
TextArea,
ValidatedOptions,
} from "@patternfly/react-core";
import { useAlerts } from "../../components/alert/Alerts";
import { Controller, useForm } from "react-hook-form";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
export const NewRoleForm = () => {
const { t } = useTranslation("roles");
const { addAlert } = useAlerts();
const adminClient = useAdminClient();
const { register, control, errors, handleSubmit } = useForm<
RoleRepresentation
>();
const save = async (role: RoleRepresentation) => {
try {
await adminClient.roles.create(role);
addAlert(t("roleCreated"), AlertVariant.success);
} catch (error) {
addAlert(`${t("roleCreateError")} '${error}'`, AlertVariant.danger);
}
};
return (
<>
<PageSection variant="light">
<TextContent>
<Text component="h1">{t("createRole")}</Text>
</TextContent>
</PageSection>
<Divider />
<PageSection variant="light">
<Form isHorizontal onSubmit={handleSubmit(save)}>
<FormGroup
label={t("roleName")}
isRequired
fieldId="kc-role-name"
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
isRequired
type="text"
id="kc-role-name"
name="name"
ref={register({ required: true })}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("description")}
fieldId="kc-role-description"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:maxLength", { length: 255 })}
>
<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:create")}
</Button>
<Button variant="link">{t("common:cancel")}</Button>
</ActionGroup>
</Form>
</PageSection>
</>
);
};

View file

@ -7,6 +7,7 @@
"type": "Type", "type": "Type",
"homeURL": "Home URL", "homeURL": "Home URL",
"roleExplain": "Realm-level roles are a global namespace to define your roles.", "roleExplain": "Realm-level roles are a global namespace to define your roles.",
"roleCreateExplain": "This is some description",
"roleName": "Role name", "roleName": "Role name",
"roleDetails": "Role details", "roleDetails": "Role details",
"composite": "Composite", "composite": "Composite",
@ -18,14 +19,14 @@
"capabilityConfig": "Capability config", "capabilityConfig": "Capability config",
"roleImportError": "Could not import role", "roleImportError": "Could not import role",
"roleCreated": "Role created", "roleCreated": "Role created",
"roleCreateError": "Could not create role:", "roleCreateError": "Could not create role: {{error}}",
"roleImportSuccess": "Role import successful", "roleImportSuccess": "Role import successful",
"roleDeleteConfirm": "Delete role?", "roleDeleteConfirm": "Delete role?",
"roleDeleteConfirmDialog": "This action will permanently delete the role {{selectedRoleName}} and cannot be undone.", "roleDeleteConfirmDialog": "This action will permanently delete the role {{selectedRoleName}} and cannot be undone.",
"roleDeletedSuccess": "The role has been deleted", "roleDeletedSuccess": "The role has been deleted",
"roleDeleteError": "Could not delete role:", "roleDeleteError": "Could not delete role:",
"roleSaveSuccess": "The role has been saved", "roleSaveSuccess": "The role has been saved",
"roleSaveError": "Could not save role:", "roleSaveError": "Could not save role: {{error}}",
"noRolesInThisRealm": "No roles in this realm", "noRolesInThisRealm": "No roles in this realm",
"noRolesInThisRealmInstructions": "You haven't created any roles in this realm. Create a role to get started.", "noRolesInThisRealmInstructions": "You haven't created any roles in this realm. Create a role to get started.",
"roleAuthentication": "Role authentication" "roleAuthentication": "Role authentication"

View file

@ -11,8 +11,7 @@ import { EventsSection } from "./events/EventsSection";
import { GroupsSection } from "./groups/GroupsSection"; import { GroupsSection } from "./groups/GroupsSection";
import { IdentityProvidersSection } from "./identity-providers/IdentityProvidersSection"; import { IdentityProvidersSection } from "./identity-providers/IdentityProvidersSection";
import { PageNotFoundSection } from "./PageNotFoundSection"; import { PageNotFoundSection } from "./PageNotFoundSection";
import { NewRoleForm } from "./realm-roles/add/NewRoleForm"; import { RealmRolesForm } from "./realm-roles/RealmRoleForm";
import { RolesForm } from "./realm-roles/RealmRoleDetails";
import { RealmRolesSection } from "./realm-roles/RealmRolesSection"; import { RealmRolesSection } from "./realm-roles/RealmRolesSection";
import { RealmSettingsSection } from "./realm-settings/RealmSettingsSection"; import { RealmSettingsSection } from "./realm-settings/RealmSettingsSection";
import { NewRealmForm } from "./realm/add/NewRealmForm"; import { NewRealmForm } from "./realm/add/NewRealmForm";
@ -106,17 +105,17 @@ export const routes: RoutesFn = (t: TFunction) => [
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/roles/:id", path: "/roles/add-role",
component: RolesForm, component: RealmRolesForm,
breadcrumb: t("roles:roleDetails"),
access: "view-realm",
},
{
path: "/add-role",
component: NewRoleForm,
breadcrumb: t("roles:createRole"), breadcrumb: t("roles:createRole"),
access: "manage-realm", access: "manage-realm",
}, },
{
path: "/roles/:id",
component: RealmRolesForm,
breadcrumb: t("roles:roleDetails"),
access: "view-realm",
},
{ {
path: "/users", path: "/users",
component: UsersSection, component: UsersSection,

View file

@ -1,17 +1,17 @@
import React from "react"; import React from "react";
import { Meta } from "@storybook/react"; import { Meta } from "@storybook/react";
import { Page } from "@patternfly/react-core"; import { Page } from "@patternfly/react-core";
import { RolesForm } from "../realm-roles/RealmRoleDetails"; import { RealmRolesForm } from "../realm-roles/RealmRoleForm";
export default { export default {
title: "Role details tab", title: "Role details tab",
component: RolesForm, component: RealmRolesForm,
} as Meta; } as Meta;
export const RoleDetailsExample = () => { export const RoleDetailsExample = () => {
return ( return (
<Page> <Page>
<RolesForm /> <RealmRolesForm />
</Page> </Page>
); );
}; };