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:
parent
bb1b48e4c8
commit
5e6c6b0775
10 changed files with 147 additions and 67 deletions
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue