Default roles: UI Changes from KEYCLOAK-14846 (#693)

* defaultRoles wip

default role changes done

clean up log stmts

update snapshot

fix masthead test

* fix cypress test

* fix tabs
This commit is contained in:
Jenny 2021-06-18 16:04:20 -04:00 committed by GitHub
parent bb1b48e4c8
commit 5e6c6b0775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 147 additions and 67 deletions

View file

@ -39,12 +39,12 @@ describe("Masthead tests in desktop mode", () => {
listingPage.goToItemDetails("address"); listingPage.goToItemDetails("address");
cy.get("#view-header-subkey").should("exist"); cy.get("#view-header-subkey").should("exist");
cy.get(`#${CSS.escape("client-scopes-help:name")}`).should("exist"); cy.get(`#name-help-icon`).should("exist");
masthead.toggleGlobalHelp(); masthead.toggleGlobalHelp();
cy.get("#view-header-subkey").should("not.exist"); cy.get("#view-header-subkey").should("not.exist");
cy.get(`#${CSS.escape("client-scopes-help:name")}`).should("not.exist"); cy.get(`#name-help-icon`).should("not.exist");
}); });
logOutTest(); logOutTest();

View file

@ -57,7 +57,7 @@ export default class AssociatedRolesPage {
cy.get(this.addAssociatedRolesModalButton).contains("Add").click(); cy.get(this.addAssociatedRolesModalButton).contains("Add").click();
cy.wait(2500); cy.wait(5000);
cy.contains("Users in role").click().get(this.usersPage).should("exist"); cy.contains("Users in role").click().get(this.usersPage).should("exist");
} }

View file

@ -66,6 +66,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
label={t("common:name")} label={t("common:name")}
labelIcon={ labelIcon={
<HelpItem <HelpItem
id="name-help-icon"
helpText="client-scopes-help:name" helpText="client-scopes-help:name"
forLabel={t("common:name")} forLabel={t("common:name")}
forID="kc-name" forID="kc-name"

View file

@ -10,12 +10,14 @@ type HelpItemProps = {
forID: string; forID: string;
noVerticalAlign?: boolean; noVerticalAlign?: boolean;
unWrap?: boolean; unWrap?: boolean;
id?: string;
}; };
export const HelpItem = ({ export const HelpItem = ({
helpText, helpText,
forLabel, forLabel,
forID, forID,
id,
noVerticalAlign = true, noVerticalAlign = true,
unWrap = false, unWrap = false,
}: HelpItemProps) => { }: HelpItemProps) => {
@ -28,7 +30,7 @@ export const HelpItem = ({
<> <>
{!unWrap && ( {!unWrap && (
<button <button
id={helpText} id={id}
aria-label={t(`helpLabel`, { label: forLabel })} aria-label={t(`helpLabel`, { label: forLabel })}
onClick={(e) => e.preventDefault()} onClick={(e) => e.preventDefault()}
aria-describedby={forID} aria-describedby={forID}

View file

@ -6,7 +6,6 @@ exports[`<HelpItem /> render 1`] = `
aria-describedby="placeholder" aria-describedby="placeholder"
aria-label="helpLabel" aria-label="helpLabel"
class="pf-c-form__group-label-help" class="pf-c-form__group-label-help"
id="storybook"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"

View file

@ -20,7 +20,7 @@ export type RealmRoleFormProps = {
}; };
export const RealmRoleForm = ({ export const RealmRoleForm = ({
form: { handleSubmit, errors, register }, form: { handleSubmit, errors, register, getValues },
save, save,
editMode, editMode,
reset, reset,
@ -59,6 +59,7 @@ export const RealmRoleForm = ({
> >
<TextArea <TextArea
name="description" name="description"
isDisabled={getValues().name?.includes("default-roles")}
ref={register({ ref={register({
maxLength: { maxLength: {
value: 255, value: 255,

View file

@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
import { useFieldArray, 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, useFetch } from "../context/auth/AdminClient";
import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import type Composites from "keycloak-admin/lib/defs/roleRepresentation"; import type Composites from "keycloak-admin/lib/defs/roleRepresentation";
import { import {
@ -29,11 +29,19 @@ import { AssociatedRolesModal } from "./AssociatedRolesModal";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { AssociatedRolesTab } from "./AssociatedRolesTab"; import { AssociatedRolesTab } from "./AssociatedRolesTab";
import { UsersInRoleTab } from "./UsersInRoleTab"; import { UsersInRoleTab } from "./UsersInRoleTab";
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & { export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
attributes: KeyValueType[]; attributes: KeyValueType[];
}; };
type myRealmRepresentation = RealmRepresentation & {
defaultRole?: {
id: string;
name: string;
};
};
export const RealmRoleTabs = () => { export const RealmRoleTabs = () => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const form = useForm<RoleFormType>({ mode: "onChange" }); const form = useForm<RoleFormType>({ mode: "onChange" });
@ -46,7 +54,7 @@ export const RealmRoleTabs = () => {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const { realm } = useRealm(); const { realm: realmName } = useRealm();
const [key, setKey] = useState(""); const [key, setKey] = useState("");
@ -69,6 +77,17 @@ export const RealmRoleTabs = () => {
}; };
}; };
const [realm, setRealm] = useState<myRealmRepresentation>();
useFetch(
() => adminClient.realms.findOne({ realm: realmName }),
(realm) => {
setRealm(realm);
},
[]
);
useEffect(() => { useEffect(() => {
const update = async () => { const update = async () => {
if (id) { if (id) {
@ -124,7 +143,7 @@ export const RealmRoleTabs = () => {
} }
await adminClient.roles.createComposite( await adminClient.roles.createComposite(
{ roleId: id, realm }, { roleId: id, realm: realmName },
additionalRoles additionalRoles
); );
@ -175,7 +194,7 @@ export const RealmRoleTabs = () => {
try { try {
await adminClient.roles.createComposite( await adminClient.roles.createComposite(
{ roleId: id, realm: realm }, { roleId: id, realm: realmName },
compositeArray compositeArray
); );
history.push(url.substr(0, url.lastIndexOf("/") + 1) + "AssociatedRoles"); history.push(url.substr(0, url.lastIndexOf("/") + 1) + "AssociatedRoles");
@ -211,6 +230,54 @@ export const RealmRoleTabs = () => {
}, },
}); });
const dropdownItems =
url.includes("AssociatedRoles") && !realm?.defaultRole
? [
<DropdownItem
key="delete-all-associated"
component="button"
onClick={() => toggleDeleteAllAssociatedRolesDialog()}
>
{t("roles:removeAllAssociatedRoles")}
</DropdownItem>,
<DropdownItem
key="delete-role"
component="button"
onClick={() => {
toggleDeleteDialog();
}}
>
{t("deleteRole")}
</DropdownItem>,
]
: id && realm?.defaultRole && url.includes("AssociatedRoles")
? [
<DropdownItem
key="delete-all-associated"
component="button"
onClick={() => toggleDeleteAllAssociatedRolesDialog()}
>
{t("roles:removeAllAssociatedRoles")}
</DropdownItem>,
]
: [
<DropdownItem
key="toggle-modal"
data-testid="add-roles"
component="button"
onClick={() => toggleModal()}
>
{t("addAssociatedRolesText")}
</DropdownItem>,
<DropdownItem
key="delete-role"
component="button"
onClick={() => toggleDeleteDialog()}
>
{t("deleteRole")}
</DropdownItem>,
];
const [ const [
toggleDeleteAllAssociatedRolesDialog, toggleDeleteAllAssociatedRolesDialog,
DeleteAllAssociatedRolesConfirm, DeleteAllAssociatedRolesConfirm,
@ -256,45 +323,7 @@ export const RealmRoleTabs = () => {
badgeIsRead={true} badgeIsRead={true}
subKey={id ? "" : "roles:roleCreateExplain"} subKey={id ? "" : "roles:roleCreateExplain"}
actionsDropdownId="roles-actions-dropdown" actionsDropdownId="roles-actions-dropdown"
divider={!id} dropdownItems={dropdownItems}
dropdownItems={
url.includes("AssociatedRoles")
? [
<DropdownItem
key="delete-all-associated"
component="button"
onClick={() => toggleDeleteAllAssociatedRolesDialog()}
>
{t("roles:removeAllAssociatedRoles")}
</DropdownItem>,
<DropdownItem
key="delete-role"
component="button"
onClick={() => toggleDeleteDialog()}
>
{t("deleteRole")}
</DropdownItem>,
]
: id
? [
<DropdownItem
key="toggle-modal"
data-testid="add-roles"
component="button"
onClick={() => toggleModal()}
>
{t("addAssociatedRolesText")}
</DropdownItem>,
<DropdownItem
key="delete-role"
component="button"
onClick={() => toggleDeleteDialog()}
>
{t("deleteRole")}
</DropdownItem>,
]
: undefined
}
/> />
<PageSection variant="light" className="pf-u-p-0"> <PageSection variant="light" className="pf-u-p-0">
{id && ( {id && (
@ -329,25 +358,28 @@ export const RealmRoleTabs = () => {
</PageSection> </PageSection>
</Tab> </Tab>
)} )}
<Tab {form.getValues().name !== realm?.defaultRole?.name && (
eventKey="attributes" <Tab
title={<TabTitleText>{t("common:attributes")}</TabTitleText>} eventKey="attributes"
> className="kc-attributes-tab"
<PageSection variant="light"> title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
>
<AttributesForm <AttributesForm
form={form} form={form}
save={save} save={save}
array={{ fields, append, remove }} array={{ fields, append, remove }}
reset={() => form.reset(role)} reset={() => form.reset(role)}
/> />
</PageSection> </Tab>
</Tab> )}
<Tab {form.getValues().name !== realm?.defaultRole?.name && (
eventKey="users-in-role" <Tab
title={<TabTitleText>{t("usersInRole")}</TabTitleText>} eventKey="users-in-role"
> title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
<UsersInRoleTab data-cy="users-in-role-tab" /> >
</Tab> <UsersInRoleTab data-cy="users-in-role-tab" />
</Tab>
)}
</KeycloakTabs> </KeycloakTabs>
)} )}
{!id && ( {!id && (

View file

@ -1,5 +1,3 @@
.kc-who-will-appear-button { .kc-who-will-appear-button {
padding-left: 0px; padding-left: 0px;
} }
@ -33,3 +31,12 @@
margin-bottom: var(--pf-global--spacer--md); margin-bottom: var(--pf-global--spacer--md);
} }
button#default-role-help-icon.pf-c-form__group-label-help {
margin-left: var(--pf-global--spacer--xs);
vertical-align: middle;
}
.pf-c-tab-content.kc-attributes-tab {
padding-left: var(--pf-global--spacer--xl);
padding-top: var(--pf-global--spacer--lg);
}

View file

@ -3,13 +3,25 @@ import { Link, useHistory, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AlertVariant, Button, ButtonVariant } from "@patternfly/react-core"; import { AlertVariant, Button, ButtonVariant } from "@patternfly/react-core";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter, upperCaseFormatter } from "../util"; import { emptyFormatter, upperCaseFormatter } from "../util";
import { useRealm } from "../context/realm-context/RealmContext";
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { HelpItem } from "../components/help-enabler/HelpItem";
import "./RealmRolesSection.css";
type myRealmRepresentation = RealmRepresentation & {
defaultRole?: {
id: string;
name: string;
};
};
type RolesListProps = { type RolesListProps = {
paginated?: boolean; paginated?: boolean;
@ -42,12 +54,32 @@ export const RolesList = ({
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const { realm: realmName } = useRealm();
const [realm, setRealm] = useState<myRealmRepresentation>();
const [selectedRole, setSelectedRole] = useState<RoleRepresentation>(); const [selectedRole, setSelectedRole] = useState<RoleRepresentation>();
useFetch(
() => adminClient.realms.findOne({ realm: realmName }),
(realm) => {
setRealm(realm);
},
[]
);
const RoleDetailLink = (role: RoleRepresentation) => ( const RoleDetailLink = (role: RoleRepresentation) => (
<> <>
<RoleLink role={role} /> <RoleLink role={role} />
{role.name?.includes("default-role") ? (
<HelpItem
helpText={t("defaultRole")}
forLabel={t("defaultRole")}
forID="kc-defaultRole"
id="default-role-help-icon"
/>
) : (
""
)}
</> </>
); );
@ -97,8 +129,12 @@ export const RolesList = ({
{ {
title: t("common:delete"), title: t("common:delete"),
onRowClick: (role) => { onRowClick: (role) => {
setSelectedRole(role); setSelectedRole(role as RoleRepresentation);
toggleDeleteDialog(); if (
(role as RoleRepresentation).name === realm!.defaultRole!.name
) {
addAlert(`${t("defaultRoleDeleteError")}`, AlertVariant.danger);
} else toggleDeleteDialog();
}, },
}, },
]} ]}

View file

@ -34,6 +34,8 @@
"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: {{error}}", "roleDeleteError": "Could not delete role: {{error}}",
"defaultRole": "This role serves as a container for both realm and client default roles. It cannot be removed.",
"defaultRoleDeleteError": "You cannot delete a default role.",
"roleSaveSuccess": "The role has been saved", "roleSaveSuccess": "The role has been saved",
"roleSaveError": "Could not save role: {{error}}", "roleSaveError": "Could not save role: {{error}}",
"noRoles": "No roles in this realm", "noRoles": "No roles in this realm",