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");
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();
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();

View file

@ -57,7 +57,7 @@ export default class AssociatedRolesPage {
cy.get(this.addAssociatedRolesModalButton).contains("Add").click();
cy.wait(2500);
cy.wait(5000);
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")}
labelIcon={
<HelpItem
id="name-help-icon"
helpText="client-scopes-help:name"
forLabel={t("common:name")}
forID="kc-name"

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
import { useFieldArray, useForm } from "react-hook-form";
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 Composites from "keycloak-admin/lib/defs/roleRepresentation";
import {
@ -29,11 +29,19 @@ import { AssociatedRolesModal } from "./AssociatedRolesModal";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { AssociatedRolesTab } from "./AssociatedRolesTab";
import { UsersInRoleTab } from "./UsersInRoleTab";
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
attributes: KeyValueType[];
};
type myRealmRepresentation = RealmRepresentation & {
defaultRole?: {
id: string;
name: string;
};
};
export const RealmRoleTabs = () => {
const { t } = useTranslation("roles");
const form = useForm<RoleFormType>({ mode: "onChange" });
@ -46,7 +54,7 @@ export const RealmRoleTabs = () => {
const { url } = useRouteMatch();
const { realm } = useRealm();
const { realm: realmName } = useRealm();
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(() => {
const update = async () => {
if (id) {
@ -124,7 +143,7 @@ export const RealmRoleTabs = () => {
}
await adminClient.roles.createComposite(
{ roleId: id, realm },
{ roleId: id, realm: realmName },
additionalRoles
);
@ -175,7 +194,7 @@ export const RealmRoleTabs = () => {
try {
await adminClient.roles.createComposite(
{ roleId: id, realm: realm },
{ roleId: id, realm: realmName },
compositeArray
);
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 [
toggleDeleteAllAssociatedRolesDialog,
DeleteAllAssociatedRolesConfirm,
@ -256,45 +323,7 @@ export const RealmRoleTabs = () => {
badgeIsRead={true}
subKey={id ? "" : "roles:roleCreateExplain"}
actionsDropdownId="roles-actions-dropdown"
divider={!id}
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
}
dropdownItems={dropdownItems}
/>
<PageSection variant="light" className="pf-u-p-0">
{id && (
@ -329,25 +358,28 @@ export const RealmRoleTabs = () => {
</PageSection>
</Tab>
)}
<Tab
eventKey="attributes"
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
>
<PageSection variant="light">
{form.getValues().name !== realm?.defaultRole?.name && (
<Tab
eventKey="attributes"
className="kc-attributes-tab"
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
>
<AttributesForm
form={form}
save={save}
array={{ fields, append, remove }}
reset={() => form.reset(role)}
/>
</PageSection>
</Tab>
<Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
>
<UsersInRoleTab data-cy="users-in-role-tab" />
</Tab>
</Tab>
)}
{form.getValues().name !== realm?.defaultRole?.name && (
<Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
>
<UsersInRoleTab data-cy="users-in-role-tab" />
</Tab>
)}
</KeycloakTabs>
)}
{!id && (

View file

@ -1,5 +1,3 @@
.kc-who-will-appear-button {
padding-left: 0px;
}
@ -33,3 +31,12 @@
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 { 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 { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
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 = {
paginated?: boolean;
@ -42,12 +54,32 @@ export const RolesList = ({
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { url } = useRouteMatch();
const { realm: realmName } = useRealm();
const [realm, setRealm] = useState<myRealmRepresentation>();
const [selectedRole, setSelectedRole] = useState<RoleRepresentation>();
useFetch(
() => adminClient.realms.findOne({ realm: realmName }),
(realm) => {
setRealm(realm);
},
[]
);
const RoleDetailLink = (role: RoleRepresentation) => (
<>
<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"),
onRowClick: (role) => {
setSelectedRole(role);
toggleDeleteDialog();
setSelectedRole(role as RoleRepresentation);
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.",
"roleDeletedSuccess": "The role has been deleted",
"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",
"roleSaveError": "Could not save role: {{error}}",
"noRoles": "No roles in this realm",