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");
|
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();
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue