From 5ba147b384291dddbd662e94682d985ff05f0e57 Mon Sep 17 00:00:00 2001 From: Jon Koops Date: Sat, 14 Jan 2023 17:05:09 +0100 Subject: [PATCH] Use routable tabs for realm role details (#3963) --- .../admin-ui/manage/clients/ClientRolesTab.ts | 4 +- .../manage/realm_roles/CreateRealmRolePage.ts | 2 +- apps/admin-ui/public/resources/en/common.json | 2 - .../components/keycloak-tabs/KeycloakTabs.tsx | 85 ------------------- .../components/routable-tabs/RoutableTabs.tsx | 11 --- .../src/realm-roles/RealmRoleTabs.tsx | 48 ++++++++--- .../src/realm-roles/routes/RealmRole.ts | 3 +- 7 files changed, 42 insertions(+), 113 deletions(-) delete mode 100644 apps/admin-ui/src/components/keycloak-tabs/KeycloakTabs.tsx diff --git a/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRolesTab.ts b/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRolesTab.ts index 5cc26fbf0b..cbd3047eb2 100644 --- a/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRolesTab.ts +++ b/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRolesTab.ts @@ -11,7 +11,7 @@ export default class ClientRolesTab extends CommonPage { private createRoleEmptyStateBtn = "no-roles-for-this-client-empty-action"; private hideInheritedRolesChkBox = "#hideInheritedRoles"; private rolesTab = "rolesTab"; - private associatedRolesTab = ".kc-associated-roles-tab > button"; + private associatedRolesTab = "associatedRolesTab"; goToDetailsTab() { this.tabUtils().clickTab(ClientRolesTabItems.Details); @@ -34,7 +34,7 @@ export default class ClientRolesTab extends CommonPage { } goToAssociatedRolesTab() { - cy.get(this.associatedRolesTab).click(); + cy.findByTestId(this.associatedRolesTab).click(); return this; } diff --git a/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_roles/CreateRealmRolePage.ts b/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_roles/CreateRealmRolePage.ts index ca9889cca0..02f50e42ee 100644 --- a/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_roles/CreateRealmRolePage.ts +++ b/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_roles/CreateRealmRolePage.ts @@ -63,7 +63,7 @@ class CreateRealmRolePage { } goToAttributesTab() { - cy.get(".kc-attributes-tab > button").click(); + cy.findByTestId("attributesTab").click(); return this; } } diff --git a/apps/admin-ui/public/resources/en/common.json b/apps/admin-ui/public/resources/en/common.json index 3c96b8f77b..4d3ff1cf3e 100644 --- a/apps/admin-ui/public/resources/en/common.json +++ b/apps/admin-ui/public/resources/en/common.json @@ -194,8 +194,6 @@ "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.", "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", "reorder": "Reorder", "onDragStart": "Dragging started for item {{item}}", diff --git a/apps/admin-ui/src/components/keycloak-tabs/KeycloakTabs.tsx b/apps/admin-ui/src/components/keycloak-tabs/KeycloakTabs.tsx deleted file mode 100644 index 744b2fc7d0..0000000000 --- a/apps/admin-ui/src/components/keycloak-tabs/KeycloakTabs.tsx +++ /dev/null @@ -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 & { - 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 - | undefined; - const [key, setKey] = useState(""); - - const firstTab = Children.toArray(children)[0]; - const tab = - params[paramName] || - (isValidElement(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 ( - <> - - { - if (form?.formState.isDirty) { - setKey(key as string); - toggleChangeTabDialog(); - } else { - navigate( - createUrl(path, { ...params, [paramName]: key as string }) - ); - } - }} - {...rest} - > - {children} - - - ); -}; diff --git a/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx b/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx index 620cbf6d1d..12c65b27d9 100644 --- a/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx +++ b/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx @@ -4,7 +4,6 @@ import { TabsComponent, TabsProps, } from "@patternfly/react-core"; -import type { History } from "history"; import { Children, isValidElement, @@ -66,16 +65,6 @@ export const RoutableTabs = ({ ); }; -type RoutableTabParams = { - to: Partial; - history: History; -}; - -export const routableTab = ({ to, history }: RoutableTabParams) => ({ - eventKey: to.pathname ?? "", - href: history.createHref(to), -}); - export const useRoutableTab = (to: Partial) => ({ eventKey: to.pathname ?? "", href: useHref(to), diff --git a/apps/admin-ui/src/realm-roles/RealmRoleTabs.tsx b/apps/admin-ui/src/realm-roles/RealmRoleTabs.tsx index 1cb5ee28f6..ce8eb6d9c7 100644 --- a/apps/admin-ui/src/realm-roles/RealmRoleTabs.tsx +++ b/apps/admin-ui/src/realm-roles/RealmRoleTabs.tsx @@ -19,6 +19,7 @@ import { toClient } from "../clients/routes/Client"; import { ClientRoleParams, ClientRoleRoute, + ClientRoleTab, toClientRole, } from "../clients/routes/ClientRole"; import { useAlerts } from "../components/alert/Alerts"; @@ -32,17 +33,20 @@ import { keyValueToArray, } from "../components/key-value-form/key-value-convert"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; -import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { PermissionsTab } from "../components/permission-tab/PermissionTab"; import { RoleForm } from "../components/role-form/RoleForm"; import { AddRoleMappingModal } from "../components/role-mapping/AddRoleMappingModal"; import { RoleMapping } from "../components/role-mapping/RoleMapping"; +import { + RoutableTabs, + useRoutableTab, +} from "../components/routable-tabs/RoutableTabs"; import { ViewHeader } from "../components/view-header/ViewHeader"; import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useParams } from "../utils/useParams"; -import { RealmRoleRoute, toRealmRole } from "./routes/RealmRole"; +import { RealmRoleRoute, RealmRoleTab, toRealmRole } from "./routes/RealmRole"; import { toRealmRoles } from "./routes/RealmRoles"; import { UsersInRoleTab } from "./UsersInRoleTab"; @@ -57,7 +61,7 @@ export default function RealmRoleTabs() { const { adminClient } = useAdminClient(); const [role, setRole] = useState(); - const { id, clientId } = useParams<{ id: string; clientId: string }>(); + const { id, clientId } = useParams(); const { pathname } = useLocation(); const { realm: realmName } = useRealm(); @@ -292,6 +296,27 @@ export default function RealmRoleTabs() { 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[]) => { try { await adminClient.roles.createComposite( @@ -339,10 +364,10 @@ export default function RealmRoleTabs() { divider={false} /> - + {t("common:details")}} + {...detailsTab} > {role.composite && ( {t("associatedRolesText")}} + {...associatedRolesTab} > {t("common:attributes")}} + {...attributesTab} > {t("usersInRole")}} + {...usersInRoleTab} > @@ -396,13 +422,13 @@ export default function RealmRoleTabs() { "ADMIN_FINE_GRAINED_AUTHZ" ) && ( {t("common:permissions")}} + {...permissionsTab} > )} - + ); diff --git a/apps/admin-ui/src/realm-roles/routes/RealmRole.ts b/apps/admin-ui/src/realm-roles/routes/RealmRole.ts index 4e0866683d..29345df253 100644 --- a/apps/admin-ui/src/realm-roles/routes/RealmRole.ts +++ b/apps/admin-ui/src/realm-roles/routes/RealmRole.ts @@ -8,7 +8,8 @@ export type RealmRoleTab = | "details" | "associated-roles" | "attributes" - | "users-in-role"; + | "users-in-role" + | "permissions"; export type RealmRoleParams = { realm: string;