Use routable tabs for realm role details (#3963)
This commit is contained in:
parent
fe4c3dc151
commit
5ba147b384
7 changed files with 42 additions and 113 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ class CreateRealmRolePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
goToAttributesTab() {
|
goToAttributesTab() {
|
||||||
cy.get(".kc-attributes-tab > button").click();
|
cy.findByTestId("attributesTab").click();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}",
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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),
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue