Use routable tabs for realm role details (#3963)

This commit is contained in:
Jon Koops 2023-01-14 17:05:09 +01:00 committed by GitHub
parent fe4c3dc151
commit 5ba147b384
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 42 additions and 113 deletions

View file

@ -11,7 +11,7 @@ export default class ClientRolesTab extends CommonPage {
private createRoleEmptyStateBtn = "no-roles-for-this-client-empty-action"; private createRoleEmptyStateBtn = "no-roles-for-this-client-empty-action";
private hideInheritedRolesChkBox = "#hideInheritedRoles"; private hideInheritedRolesChkBox = "#hideInheritedRoles";
private rolesTab = "rolesTab"; private rolesTab = "rolesTab";
private associatedRolesTab = ".kc-associated-roles-tab > button"; private associatedRolesTab = "associatedRolesTab";
goToDetailsTab() { goToDetailsTab() {
this.tabUtils().clickTab(ClientRolesTabItems.Details); this.tabUtils().clickTab(ClientRolesTabItems.Details);
@ -34,7 +34,7 @@ export default class ClientRolesTab extends CommonPage {
} }
goToAssociatedRolesTab() { goToAssociatedRolesTab() {
cy.get(this.associatedRolesTab).click(); cy.findByTestId(this.associatedRolesTab).click();
return this; return this;
} }

View file

@ -63,7 +63,7 @@ class CreateRealmRolePage {
} }
goToAttributesTab() { goToAttributesTab() {
cy.get(".kc-attributes-tab > button").click(); cy.findByTestId("attributesTab").click();
return this; return this;
} }
} }

View file

@ -194,8 +194,6 @@
"emptyMappers": "No mappers", "emptyMappers": "No mappers",
"emptyMappersInstructions": "If you want to add mappers, please click the button below to add some predefined mappers or to configure a new mapper.", "emptyMappersInstructions": "If you want to add mappers, please click the button below to add some predefined mappers or to configure a new mapper.",
"emptyPrimaryAction": "Add predefined mapper", "emptyPrimaryAction": "Add predefined mapper",
"leaveDirtyTitle": "Leave without saving?",
"leaveDirtyConfirm": "Do you want to leave this page without saving? Any unsaved changes will be lost.",
"leave": "Leave", "leave": "Leave",
"reorder": "Reorder", "reorder": "Reorder",
"onDragStart": "Dragging started for item {{item}}", "onDragStart": "Dragging started for item {{item}}",

View file

@ -1,85 +0,0 @@
import { Children, isValidElement, useState } from "react";
import { useRouteMatch } from "react-router-dom";
import { useNavigate } from "react-router-dom-v5-compat";
import { TabProps, Tabs, TabsProps } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
type KeycloakTabsProps = Omit<TabsProps, "ref" | "activeKey" | "onSelect"> & {
paramName?: string;
};
const createUrl = (
path: string,
params: { [index: string]: string }
): string => {
let url = path;
for (const key in params) {
const value = params[key];
if (url.includes(key)) {
url = url.replace(new RegExp(`:${key}\\??`), value || "");
}
}
return url;
};
export const KeycloakTabs = ({
paramName = "tab",
children,
...rest
}: KeycloakTabsProps) => {
const match = useRouteMatch();
const params = match.params as { [index: string]: string };
const navigate = useNavigate();
const form = useFormContext() as
| ReturnType<typeof useFormContext>
| undefined;
const [key, setKey] = useState("");
const firstTab = Children.toArray(children)[0];
const tab =
params[paramName] ||
(isValidElement<TabProps>(firstTab) && firstTab.props.eventKey) ||
"";
const pathIndex = match.path.indexOf(paramName) + paramName.length;
const path = match.path.substr(0, pathIndex);
const [toggleChangeTabDialog, ChangeTabConfirm] = useConfirmDialog({
titleKey: "common:leaveDirtyTitle",
messageKey: "common:leaveDirtyConfirm",
continueButtonLabel: "common:leave",
onConfirm: () => {
form?.reset();
navigate(createUrl(path, { ...params, [paramName]: key as string }));
},
});
return (
<>
<ChangeTabConfirm />
<Tabs
inset={{
default: "insetNone",
md: "insetSm",
xl: "inset2xl",
"2xl": "insetLg",
}}
activeKey={tab}
onSelect={(_, key) => {
if (form?.formState.isDirty) {
setKey(key as string);
toggleChangeTabDialog();
} else {
navigate(
createUrl(path, { ...params, [paramName]: key as string })
);
}
}}
{...rest}
>
{children}
</Tabs>
</>
);
};

View file

@ -4,7 +4,6 @@ import {
TabsComponent, TabsComponent,
TabsProps, TabsProps,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type { History } from "history";
import { import {
Children, Children,
isValidElement, isValidElement,
@ -66,16 +65,6 @@ export const RoutableTabs = ({
); );
}; };
type RoutableTabParams = {
to: Partial<Path>;
history: History<unknown>;
};
export const routableTab = ({ to, history }: RoutableTabParams) => ({
eventKey: to.pathname ?? "",
href: history.createHref(to),
});
export const useRoutableTab = (to: Partial<Path>) => ({ export const useRoutableTab = (to: Partial<Path>) => ({
eventKey: to.pathname ?? "", eventKey: to.pathname ?? "",
href: useHref(to), href: useHref(to),

View file

@ -19,6 +19,7 @@ import { toClient } from "../clients/routes/Client";
import { import {
ClientRoleParams, ClientRoleParams,
ClientRoleRoute, ClientRoleRoute,
ClientRoleTab,
toClientRole, toClientRole,
} from "../clients/routes/ClientRole"; } from "../clients/routes/ClientRole";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
@ -32,17 +33,20 @@ import {
keyValueToArray, keyValueToArray,
} from "../components/key-value-form/key-value-convert"; } from "../components/key-value-form/key-value-convert";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { PermissionsTab } from "../components/permission-tab/PermissionTab"; import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import { RoleForm } from "../components/role-form/RoleForm"; import { RoleForm } from "../components/role-form/RoleForm";
import { AddRoleMappingModal } from "../components/role-mapping/AddRoleMappingModal"; import { AddRoleMappingModal } from "../components/role-mapping/AddRoleMappingModal";
import { RoleMapping } from "../components/role-mapping/RoleMapping"; import { RoleMapping } from "../components/role-mapping/RoleMapping";
import {
RoutableTabs,
useRoutableTab,
} from "../components/routable-tabs/RoutableTabs";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { useParams } from "../utils/useParams"; import { useParams } from "../utils/useParams";
import { RealmRoleRoute, toRealmRole } from "./routes/RealmRole"; import { RealmRoleRoute, RealmRoleTab, toRealmRole } from "./routes/RealmRole";
import { toRealmRoles } from "./routes/RealmRoles"; import { toRealmRoles } from "./routes/RealmRoles";
import { UsersInRoleTab } from "./UsersInRoleTab"; import { UsersInRoleTab } from "./UsersInRoleTab";
@ -57,7 +61,7 @@ export default function RealmRoleTabs() {
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const [role, setRole] = useState<AttributeForm>(); const [role, setRole] = useState<AttributeForm>();
const { id, clientId } = useParams<{ id: string; clientId: string }>(); const { id, clientId } = useParams<ClientRoleParams>();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { realm: realmName } = useRealm(); const { realm: realmName } = useRealm();
@ -292,6 +296,27 @@ export default function RealmRoleTabs() {
navigate(to); navigate(to);
}; };
const toTab = (tab: RealmRoleTab | ClientRoleTab) =>
clientRoleRouteMatch
? toClientRole({
...clientRoleRouteMatch.params,
tab: tab as ClientRoleTab,
})
: toRealmRole({
realm: realmName,
id,
tab,
});
const useTab = (tab: RealmRoleTab | ClientRoleTab) =>
useRoutableTab(toTab(tab));
const detailsTab = useTab("details");
const associatedRolesTab = useTab("associated-roles");
const attributesTab = useTab("attributes");
const usersInRoleTab = useTab("users-in-role");
const permissionsTab = useTab("permissions");
const addComposites = async (composites: RoleRepresentation[]) => { const addComposites = async (composites: RoleRepresentation[]) => {
try { try {
await adminClient.roles.createComposite( await adminClient.roles.createComposite(
@ -339,10 +364,10 @@ export default function RealmRoleTabs() {
divider={false} divider={false}
/> />
<PageSection variant="light" className="pf-u-p-0"> <PageSection variant="light" className="pf-u-p-0">
<KeycloakTabs isBox mountOnEnter> <RoutableTabs isBox mountOnEnter defaultLocation={toTab("details")}>
<Tab <Tab
eventKey="details"
title={<TabTitleText>{t("common:details")}</TabTitleText>} title={<TabTitleText>{t("common:details")}</TabTitleText>}
{...detailsTab}
> >
<RoleForm <RoleForm
form={form} form={form}
@ -358,9 +383,9 @@ export default function RealmRoleTabs() {
</Tab> </Tab>
{role.composite && ( {role.composite && (
<Tab <Tab
eventKey="associated-roles" data-testid="associatedRolesTab"
className="kc-associated-roles-tab"
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>} title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
{...associatedRolesTab}
> >
<RoleMapping <RoleMapping
name={role.name!} name={role.name!}
@ -373,9 +398,10 @@ export default function RealmRoleTabs() {
)} )}
{!isDefaultRole(role.name!) && ( {!isDefaultRole(role.name!) && (
<Tab <Tab
eventKey="attributes" data-testid="attributesTab"
className="kc-attributes-tab" className="kc-attributes-tab"
title={<TabTitleText>{t("common:attributes")}</TabTitleText>} title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
{...attributesTab}
> >
<AttributesForm <AttributesForm
form={form} form={form}
@ -386,8 +412,8 @@ export default function RealmRoleTabs() {
)} )}
{!isDefaultRole(role.name!) && ( {!isDefaultRole(role.name!) && (
<Tab <Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>} title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
{...usersInRoleTab}
> >
<UsersInRoleTab data-cy="users-in-role-tab" /> <UsersInRoleTab data-cy="users-in-role-tab" />
</Tab> </Tab>
@ -396,13 +422,13 @@ export default function RealmRoleTabs() {
"ADMIN_FINE_GRAINED_AUTHZ" "ADMIN_FINE_GRAINED_AUTHZ"
) && ( ) && (
<Tab <Tab
eventKey="permissions"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>} title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
{...permissionsTab}
> >
<PermissionsTab id={role.id} type="roles" /> <PermissionsTab id={role.id} type="roles" />
</Tab> </Tab>
)} )}
</KeycloakTabs> </RoutableTabs>
</PageSection> </PageSection>
</> </>
); );

View file

@ -8,7 +8,8 @@ export type RealmRoleTab =
| "details" | "details"
| "associated-roles" | "associated-roles"
| "attributes" | "attributes"
| "users-in-role"; | "users-in-role"
| "permissions";
export type RealmRoleParams = { export type RealmRoleParams = {
realm: string; realm: string;