Use realm name in urls (#265)

* remove circular dependency on realm context

* added realm as a param of the url

* updated links to include realm

* null !== undefined

* set realm if realm in url

* fixed breadcrumb type

* fixed tests

* addressed pr review comments
This commit is contained in:
Erik Jan de Wit 2021-01-05 20:49:33 +01:00 committed by GitHub
parent 27d9dadee7
commit b14027ccb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 697 additions and 589 deletions

View file

@ -30,7 +30,7 @@
"react-hook-form": "^6.8.2",
"react-i18next": "^11.7.0",
"react-router-dom": "^5.2.0",
"use-react-router-breadcrumbs": "^1.0.4"
"use-react-router-breadcrumbs": "^1.0.5"
},
"devDependencies": {
"@babel/core": "^7.10.5",

View file

@ -1,6 +1,11 @@
import React, { ReactNode } from "react";
import React, { ReactNode, useEffect } from "react";
import { Page } from "@patternfly/react-core";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import {
BrowserRouter as Router,
Route,
Switch,
useParams,
} from "react-router-dom";
import { Header } from "./PageHeader";
import { PageNav } from "./PageNav";
@ -13,6 +18,7 @@ import { AccessContextProvider, useAccess } from "./context/access/Access";
import { routes, RouteDef } from "./route-config";
import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
import { ForbiddenSection } from "./ForbiddenSection";
import { useRealm } from "./context/realm-context/RealmContext";
// This must match the id given as scrollableSelector in scroll-form
const mainPageContentId = "kc-main-content-page-container";
@ -31,6 +37,13 @@ const AppContexts = ({ children }: { children: ReactNode }) => (
// have access to, show forbidden page.
type SecuredRouteProps = { route: RouteDef };
const SecuredRoute = ({ route }: SecuredRouteProps) => {
const { setRealm } = useRealm();
const { realm } = useParams<{ realm: string }>();
useEffect(() => {
if (realm) {
setRealm(realm);
}
}, []);
const { hasAccess } = useAccess();
if (hasAccess(route.access)) return <route.component />;

View file

@ -14,12 +14,12 @@ export const KeycloakAdminConsole = ({
adminClient,
}: KeycloakAdminConsoleProps) => {
return (
<AdminClient.Provider value={adminClient}>
<WhoAmIContextProvider>
<RealmContextProvider>
<RealmContextProvider>
<AdminClient.Provider value={adminClient}>
<WhoAmIContextProvider>
<App />
</RealmContextProvider>
</WhoAmIContextProvider>
</AdminClient.Provider>
</WhoAmIContextProvider>
</AdminClient.Provider>
</RealmContextProvider>
);
};

View file

@ -9,6 +9,7 @@ import {
PageSidebar,
} from "@patternfly/react-core";
import { RealmSelector } from "./components/realm-selector/RealmSelector";
import { useRealm } from "./context/realm-context/RealmContext";
import { DataLoader } from "./components/data-loader/DataLoader";
import { useAdminClient } from "./context/auth/AdminClient";
import { useAccess } from "./context/access/Access";
@ -44,13 +45,16 @@ export const PageNav: React.FunctionComponent = () => {
type LeftNavProps = { title: string; path: string };
const LeftNav = ({ title, path }: LeftNavProps) => {
const route = routes(() => {}).find((route) => route.path === path);
const { realm } = useRealm();
const route = routes(() => {}).find(
(route) => route.path.substr("/:realm".length) === path
);
if (!route || !hasAccess(route.access)) return <></>;
return (
<NavItem
id={"nav-item" + path.replace("/", "-")}
to={path}
to={`/${realm}${path}`}
isActive={activeItem === path}
>
{t(title)}

View file

@ -1,6 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import { Link, useHistory, useRouteMatch } from "react-router-dom";
import { Button, PageSection } from "@patternfly/react-core";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
@ -11,6 +11,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
export const ClientScopesSection = () => {
const { t } = useTranslation("client-scopes");
const history = useHistory();
const { url } = useRouteMatch();
const adminClient = useAdminClient();
@ -18,7 +19,7 @@ export const ClientScopesSection = () => {
const ClientScopeDetailLink = (clientScope: ClientScopeRepresentation) => (
<>
<Link key={clientScope.id} to={`/client-scopes/${clientScope.id}`}>
<Link key={clientScope.id} to={`${url}/${clientScope.id}`}>
{clientScope.name}
</Link>
</>
@ -35,7 +36,7 @@ export const ClientScopesSection = () => {
ariaLabelKey="client-scopes:clientScopeList"
searchPlaceholderKey="client-scopes:searchFor"
toolbarItem={
<Button onClick={() => history.push("/client-scopes/new")}>
<Button onClick={() => history.push(`${url}/new`)}>
{t("createClientScope")}
</Button>
}

View file

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory } from "react-router-dom";
import { Link, useHistory, useRouteMatch } from "react-router-dom";
import {
AlertVariant,
ButtonVariant,
@ -44,6 +44,7 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const history = useHistory();
const { url } = useRouteMatch();
const [filteredData, setFilteredData] = useState<
{ mapper: ProtocolMapperRepresentation; cells: Row }[]
@ -70,7 +71,7 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
): Promise<void> => {
if (filter === undefined) {
const mapper = mappers as ProtocolMapperTypeRepresentation;
history.push(`/client-scopes/${clientScope.id}/${mapper.id}`);
history.push(`${url}/${mapper.id}`);
} else {
try {
await adminClient.clientScopes.addMultipleProtocolMappers(
@ -122,7 +123,7 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
cells: {
name: (
<>
<Link to={`/client-scopes/${clientScope.id}/${mapper.id}`}>
<Link to={`${url}/${clientScope.id}/${mapper.id}`}>
{mapper.name}
</Link>
</>

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
ActionGroup,
@ -52,6 +52,7 @@ export const MappingDetails = () => {
const history = useHistory();
const serverInfo = useServerInfo();
const { url } = useRouteMatch();
const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/;
useEffect(() => {
@ -102,7 +103,7 @@ export const MappingDetails = () => {
[]
);
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
history.push(`/client-scopes/${scopeId}`);
history.push(`${url}/${scopeId}`);
} catch (error) {
addAlert(t("mappingDeletedError", { error }), AlertVariant.danger);
}

View file

@ -307,7 +307,11 @@ export const ClientScopeForm = () => {
</ActionGroup>
</Form>
</Tab>
<Tab eventKey={1} title={<TabTitleText>{t("mappers")}</TabTitleText>}>
<Tab
isHidden={!id}
eventKey={1}
title={<TabTitleText>{t("mappers")}</TabTitleText>}
>
{clientScope && (
<MapperList clientScope={clientScope} refresh={refresh} />
)}

View file

@ -1,5 +1,5 @@
import React from "react";
import { Link, useHistory } from "react-router-dom";
import { Link, useHistory, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
AlertVariant,
@ -21,6 +21,7 @@ export const ClientsSection = () => {
const { t } = useTranslation("clients");
const { addAlert } = useAlerts();
const history = useHistory();
const { url } = useRouteMatch();
const adminClient = useAdminClient();
const baseUrl = adminClient.keycloak
@ -51,7 +52,7 @@ export const ClientsSection = () => {
const ClientDetailLink = (client: ClientRepresentation) => (
<>
<Link key={client.id} to={`/clients/${client.id}`}>
<Link key={client.id} to={`${url}/${client.id}`}>
{client.clientId}
{!client.enabled && <Badge isRead>Disabled</Badge>}
</Link>
@ -71,11 +72,11 @@ export const ClientsSection = () => {
searchPlaceholderKey="clients:searchForClient"
toolbarItem={
<>
<Button onClick={() => history.push("/add-client")}>
<Button onClick={() => history.push(`${url}/add-client`)}>
{t("createClient")}
</Button>
<Button
onClick={() => history.push("/import-client")}
onClick={() => history.push(`${url}/import-client`)}
variant="link"
>
{t("importClient")}

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useHistory, useRouteMatch } from "react-router-dom";
import {
PageSection,
Wizard,
@ -89,6 +89,7 @@ export const NewClientForm = () => {
);
const title = t("createClient");
const { url } = useRouteMatch();
return (
<>
<ViewHeader
@ -97,7 +98,7 @@ export const NewClientForm = () => {
/>
<PageSection variant="light">
<Wizard
onClose={() => history.push("/clients")}
onClose={() => history.push(`${url}/clients`)}
navAriaLabel={`${title} steps`}
mainAriaLabel={`${title} content`}
steps={[

View file

@ -4,11 +4,15 @@ import useBreadcrumbs from "use-react-router-breadcrumbs";
import { useTranslation } from "react-i18next";
import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core";
import { useRealm } from "../../context/realm-context/RealmContext";
import { routes } from "../../route-config";
export const PageBreadCrumbs = () => {
const { t } = useTranslation();
const crumbs = useBreadcrumbs(routes(t), { excludePaths: ["/"] });
const { realm } = useRealm();
const crumbs = useBreadcrumbs(routes(t), {
excludePaths: ["/", `/${realm}`],
});
return (
<>
{crumbs.length > 1 && (

View file

@ -6,7 +6,7 @@ import { MemoryRouter } from "react-router-dom";
describe("BreadCrumbs tests", () => {
it("couple of crumbs", () => {
const crumbs = mount(
<MemoryRouter initialEntries={["/clients/1234"]}>
<MemoryRouter initialEntries={["/master/clients/1234"]}>
<PageBreadCrumbs />
</MemoryRouter>
);

View file

@ -22,20 +22,20 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
className="pf-c-breadcrumb__item"
>
<Link
to="/clients"
to="/master"
>
<LinkAnchor
href="/clients"
href="/master"
navigate={[Function]}
>
<a
href="/clients"
href="/master"
onClick={[Function]}
>
<span
key="/clients"
key="/master"
>
Clients
Home
</span>
</a>
</LinkAnchor>
@ -43,7 +43,7 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
</li>
</BreadcrumbItem>
<BreadcrumbItem
isActive={true}
isActive={false}
key=".$1"
showDivider={true}
>
@ -78,8 +78,65 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
</svg>
</AngleRightIcon>
</span>
<Link
to="/master/clients"
>
<LinkAnchor
href="/master/clients"
navigate={[Function]}
>
<a
href="/master/clients"
onClick={[Function]}
>
<span
key="/master/clients"
>
Clients
</span>
</a>
</LinkAnchor>
</Link>
</li>
</BreadcrumbItem>
<BreadcrumbItem
isActive={true}
key=".$2"
showDivider={true}
>
<li
className="pf-c-breadcrumb__item"
>
<span
key="/clients/1234"
className="pf-c-breadcrumb__item-divider"
>
<AngleRightIcon
color="currentColor"
noVerticalAlign={false}
size="sm"
>
<svg
aria-hidden={true}
aria-labelledby={null}
fill="currentColor"
height="1em"
role="img"
style={
Object {
"verticalAlign": "-0.125em",
}
}
viewBox="0 0 256 512"
width="1em"
>
<path
d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"
/>
</svg>
</AngleRightIcon>
</span>
<span
key="/master/clients/1234"
>
Client details
</span>

View file

@ -13,7 +13,7 @@ import {
FlexItem,
} from "@patternfly/react-core";
import "./keycloak-card.css";
import { useHistory } from "react-router-dom";
import { useHistory, useRouteMatch } from "react-router-dom";
export type KeycloakCardProps = {
id: string;
@ -38,6 +38,7 @@ export const KeycloakCard = ({
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const history = useHistory();
const { url } = useRouteMatch();
const onDropdownToggle = () => {
setIsDropdownOpen(!isDropdownOpen);
@ -48,11 +49,7 @@ export const KeycloakCard = ({
};
const openSettings = () => {
if (providerId === "kerberos") {
history.push(`/user-federation/Kerberos/${id}`);
} else {
history.push(`/user-federation/LDAP/${id}`);
}
history.push(`${url}/${providerId}/${id}`);
};
return (

View file

@ -1,5 +1,5 @@
import React, { useState, useContext, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { useHistory, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
@ -16,7 +16,7 @@ import {
import { CheckIcon } from "@patternfly/react-icons";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { useRealm } from "../../context/realm-context/RealmContext";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import "./realm-selector.css";
@ -26,13 +26,14 @@ type RealmSelectorProps = {
};
export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
const { realm, setRealm } = useContext(RealmContext);
const { realm, setRealm } = useRealm();
const whoami = useContext(WhoAmIContext);
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [filteredItems, setFilteredItems] = useState(realmList);
const history = useHistory();
const { t } = useTranslation("common");
const { url } = useRouteMatch();
const toUpperCase = (realmName: string) =>
realmName.charAt(0).toUpperCase() + realmName.slice(1);
@ -48,7 +49,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
<Button
component="div"
isBlock
onClick={() => history.push("/add-realm")}
onClick={() => history.push(`${url}/add-realm"`)}
className={className}
>
{t("createRealm")}
@ -74,6 +75,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
key={`realm-dropdown-item-${r.realm}`}
onClick={() => {
setRealm(r.realm!);
history.push(`/${r.realm}`);
setOpen(!open);
}}
>

View file

@ -4,20 +4,23 @@ import { act } from "@testing-library/react";
import { RealmSelector } from "../RealmSelector";
import { RealmContextProvider } from "../../../context/realm-context/RealmContext";
import { MemoryRouter } from "react-router-dom";
it("renders realm selector", async () => {
const wrapper = mount(
<RealmContextProvider>
<RealmSelector realmList={[{ id: "321", realm: "another" }]} />
</RealmContextProvider>
<MemoryRouter>
<RealmContextProvider>
<div id="realm">
<RealmSelector realmList={[{ id: "321", realm: "another" }]} />
</div>
</RealmContextProvider>
</MemoryRouter>
);
expect(wrapper.text()).toBe("Master");
const expandButton = wrapper.find("button");
act(() => {
expandButton!.simulate("click");
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find("#realm")).toMatchSnapshot();
});

View file

@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders realm selector 1`] = `
<RealmContextProvider>
<div
id="realm"
>
<RealmSelector
realmList={
Array [
@ -34,7 +36,7 @@ exports[`renders realm selector 1`] = `
id="realm-select-toggle"
onToggle={[Function]}
>
Master
</DropdownToggle>
}
>
@ -67,7 +69,7 @@ exports[`renders realm selector 1`] = `
id="realm-select-toggle"
onToggle={[Function]}
>
Master
</DropdownToggle>
}
>
@ -107,11 +109,7 @@ exports[`renders realm selector 1`] = `
id="realm-select-toggle"
type="button"
>
<span
class="pf-c-dropdown__toggle-text"
>
Master
</span>
<span
class="pf-c-dropdown__toggle-icon"
>
@ -201,11 +199,7 @@ exports[`renders realm selector 1`] = `
id="realm-select-toggle"
type="button"
>
<span
class="pf-c-dropdown__toggle-text"
>
Master
</span>
<span
class="pf-c-dropdown__toggle-icon"
>
@ -272,11 +266,6 @@ exports[`renders realm selector 1`] = `
onKeyDown={[Function]}
type="button"
>
<span
className="pf-c-dropdown__toggle-text"
>
Master
</span>
<span
className="pf-c-dropdown__toggle-icon"
>
@ -312,5 +301,5 @@ exports[`renders realm selector 1`] = `
</DropdownWithContext>
</Dropdown>
</RealmSelector>
</RealmContextProvider>
</div>
`;

View file

@ -1,9 +1,13 @@
import React, { useState, useContext } from "react";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import React, { useContext, useState } from "react";
export const RealmContext = React.createContext({
type RealmContextType = {
realm: string;
setRealm: (realm: string) => void;
};
export const RealmContext = React.createContext<RealmContextType>({
realm: "",
setRealm: (realm: string) => {},
setRealm: () => {},
});
type RealmContextProviderProps = { children: React.ReactNode };
@ -11,8 +15,7 @@ type RealmContextProviderProps = { children: React.ReactNode };
export const RealmContextProvider = ({
children,
}: RealmContextProviderProps) => {
const homeRealm = useContext(WhoAmIContext).getHomeRealm();
const [realm, setRealm] = useState(homeRealm);
const [realm, setRealm] = useState("");
return (
<RealmContext.Provider value={{ realm, setRealm }}>
@ -20,3 +23,5 @@ export const RealmContextProvider = ({
</RealmContext.Provider>
);
};
export const useRealm = () => useContext(RealmContext);

View file

@ -3,6 +3,7 @@ import i18n from "../../i18n";
import { DataLoader } from "../../components/data-loader/DataLoader";
import { AdminClient } from "../auth/AdminClient";
import { RealmContext } from "../realm-context/RealmContext";
import WhoAmIRepresentation, {
AccessType,
} from "keycloak-admin/lib/defs/whoAmIRepresentation";
@ -54,17 +55,23 @@ export const WhoAmIContext = React.createContext(new WhoAmI());
type WhoAmIProviderProps = { children: React.ReactNode };
export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
const adminClient = useContext(AdminClient)!;
const { realm, setRealm } = useContext(RealmContext);
const whoAmILoader = async () => {
return await adminClient.whoAmI.find();
const whoamiResponse = await adminClient.whoAmI.find({
realm: adminClient.keycloak?.realm,
});
const whoAmI = new WhoAmI(adminClient.keycloak?.realm, whoamiResponse);
if (!realm) {
setRealm(whoAmI.getHomeRealm());
}
return whoAmI;
};
return (
<DataLoader loader={whoAmILoader}>
{(whoamirep) => (
<WhoAmIContext.Provider
value={new WhoAmI(adminClient.keycloak?.realm, whoamirep.data)}
>
{(whoami) => (
<WhoAmIContext.Provider value={whoami.data}>
{children}
</WhoAmIContext.Provider>
)}

View file

@ -21,7 +21,7 @@ import { useAlerts } from "../components/alert/Alerts";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import "./GroupsSection.css";
import { Link } from "react-router-dom";
import { Link, useRouteMatch } from "react-router-dom";
type GroupTableData = GroupRepresentation & {
membersLength?: number;
@ -35,6 +35,7 @@ export const GroupsSection = () => {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
const { addAlert } = useAlerts();
const { url } = useRouteMatch();
const [key, setKey] = useState("");
const refresh = () => setKey(`${new Date().getTime()}`);
@ -82,7 +83,7 @@ export const GroupsSection = () => {
const GroupNameCell = (group: GroupTableData) => (
<>
<Link key={group.id} to={`/groups/${group.id}`}>
<Link key={group.id} to={`${url}/${group.id}`}>
{group.name}
</Link>
</>

View file

@ -27,6 +27,7 @@ import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { RoleAttributes } from "./RoleAttributes";
import { useRealm } from "../context/realm-context/RealmContext";
type RoleFormType = {
form?: UseFormMethods;
@ -37,6 +38,7 @@ type RoleFormType = {
export const RoleForm = ({ form, save, editMode }: RoleFormType) => {
const { t } = useTranslation("roles");
const history = useHistory();
const { realm } = useRealm();
return (
<FormAccess
isHorizontal
@ -90,7 +92,7 @@ export const RoleForm = ({ form, save, editMode }: RoleFormType) => {
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button variant="link" onClick={() => history.push("/roles/")}>
<Button variant="link" onClick={() => history.push(`/${realm}/roles`)}>
{editMode ? t("common:reload") : t("common:cancel")}
</Button>
</ActionGroup>
@ -104,6 +106,7 @@ export const RealmRolesForm = () => {
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const history = useHistory();
const { realm } = useRealm();
const { id } = useParams<{ id: string }>();
const [name, setName] = useState("");
@ -143,7 +146,7 @@ export const RealmRolesForm = () => {
const createdRole = await adminClient.roles.findOneByName({
name: role.name!,
});
history.push(`/roles/${createdRole.id}`);
history.push(`/${realm}/roles/${createdRole.id}`);
}
addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
} catch (error) {
@ -163,7 +166,7 @@ export const RealmRolesForm = () => {
try {
await adminClient.roles.delById({ id });
addAlert(t("roleDeletedSuccess"), AlertVariant.success);
history.push("/roles");
history.push(`/${realm}/roles`);
} catch (error) {
addAlert(`${t("roleDeleteError")} ${error}`, AlertVariant.danger);
}

View file

@ -21,6 +21,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { RoleAttributes } from "./RoleAttributes";
import "./RealmRolesSection.css";
import { useRealm } from "../context/realm-context/RealmContext";
export const RolesTabs = () => {
const { t } = useTranslation("roles");
@ -29,6 +30,7 @@ export const RolesTabs = () => {
const [name, setName] = useState("");
const adminClient = useAdminClient();
const [activeTab, setActiveTab] = useState(0);
const { realm } = useRealm();
const { id } = useParams<{ id: string }>();
@ -118,7 +120,10 @@ export const RolesTabs = () => {
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button variant="link" onClick={() => history.push("/roles/")}>
<Button
variant="link"
onClick={() => history.push(`/${realm}/roles`)}
>
{t("common:reload")}
</Button>
</ActionGroup>
@ -133,7 +138,10 @@ export const RolesTabs = () => {
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button variant="link" onClick={() => history.push("/roles/")}>
<Button
variant="link"
onClick={() => history.push(`/${realm}/roles`)}
>
{t("common:reload")}
</Button>
</ActionGroup>

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { Link, useHistory, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
AlertVariant,
@ -23,6 +23,7 @@ export const RealmRolesSection = () => {
const history = useHistory();
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { url } = useRouteMatch();
const [selectedRole, setSelectedRole] = useState<RoleRepresentation>();
@ -37,7 +38,7 @@ export const RealmRolesSection = () => {
const RoleDetailLink = (role: RoleRepresentation) => (
<>
<Link key={role.id} to={`/roles/${role.id}`}>
<Link key={role.id} to={`${url}/${role.id}`}>
{role.name}
</Link>
</>
@ -81,7 +82,7 @@ export const RealmRolesSection = () => {
},
});
const goToCreate = () => history.push("/roles/add-role");
const goToCreate = () => history.push(`${url}/add-role`);
return (
<>
<ViewHeader titleKey="roles:title" subKey="roles:roleExplain" />

View file

@ -23,173 +23,178 @@ import { ClientDetails } from "./clients/ClientDetails";
import { UserFederationKerberosSettings } from "./user-federation/UserFederationKerberosSettings";
import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings";
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
import { BreadcrumbsRoute } from "use-react-router-breadcrumbs";
export type RouteDef = {
path: string;
export type RouteDef = BreadcrumbsRoute & {
component: () => JSX.Element;
breadcrumb: TFunction | "";
access: AccessType;
};
type RoutesFn = (t: TFunction) => RouteDef[];
export const routes: RoutesFn = (t: any) => [
export const routes: RoutesFn = (t) => [
{
path: "/add-realm",
path: "/:realm/add-realm",
component: NewRealmForm,
breadcrumb: t("realm:createRealm"),
access: "manage-realm",
},
{
path: "/clients/:id",
component: ClientDetails,
breadcrumb: t("clients:clientSettings"),
access: "view-clients",
},
{
path: "/clients",
path: "/:realm/clients",
component: ClientsSection,
breadcrumb: t("clients:clientList"),
access: "query-clients",
},
{
path: "/add-client",
path: "/:realm/clients/add-client",
component: NewClientForm,
breadcrumb: t("clients:createClient"),
access: "manage-clients",
},
{
path: "/import-client",
path: "/:realm/clients/import-client",
component: ImportForm,
breadcrumb: t("clients:importClient"),
access: "manage-clients",
},
{
path: "/client-scopes/new",
path: "/:realm/clients/:id",
component: ClientDetails,
breadcrumb: t("clients:clientSettings"),
access: "view-clients",
},
{
path: "/:realm/client-scopes/new",
component: ClientScopeForm,
breadcrumb: t("client-scopes:createClientScope"),
access: "manage-clients",
},
{
path: "/client-scopes/:id",
path: "/:realm/client-scopes/:id",
component: ClientScopeForm,
breadcrumb: t("client-scopes:clientScopeDetails"),
access: "view-clients",
},
{
path: "/client-scopes/:scopeId/oidc-role-name-mapper",
path: "/:realm/client-scopes/:scopeId/oidc-role-name-mapper",
component: RoleMappingForm,
breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients",
},
{
path: "/client-scopes/:scopeId/:id",
path: "/:realm/client-scopes/:scopeId/:id",
component: MappingDetails,
breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients",
},
{
path: "/client-scopes/:id",
path: "/:realm/client-scopes/:id",
component: ClientScopeForm,
breadcrumb: t("client-scopes:clientScopeDetails"),
access: "view-clients",
},
{
path: "/client-scopes",
path: "/:realm/client-scopes",
component: ClientScopesSection,
breadcrumb: t("client-scopes:clientScopeList"),
access: "view-clients",
},
{
path: "/roles",
path: "/:realm/roles",
component: RealmRolesSection,
breadcrumb: t("roles:roleList"),
access: "view-realm",
},
{
path: "/roles/add-role",
path: "/:realm/roles/add-role",
component: RealmRolesForm,
breadcrumb: t("roles:createRole"),
access: "manage-realm",
},
{
path: "/roles/:id",
path: "/:realm/roles/:id",
component: RealmRolesForm,
breadcrumb: t("roles:roleDetails"),
access: "view-realm",
},
{
path: "/users",
path: "/:realm/users",
component: UsersSection,
breadcrumb: t("users:title"),
access: "query-users",
},
{
path: "/groups",
path: "/:realm/groups",
component: GroupsSection,
breadcrumb: t("groups"),
access: "query-groups",
},
{
path: "/sessions",
path: "/:realm/sessions",
component: SessionsSection,
breadcrumb: t("sessions:title"),
access: "view-realm",
},
{
path: "/events",
path: "/:realm/events",
component: EventsSection,
breadcrumb: t("events:title"),
access: "view-events",
},
{
path: "/realm-settings",
path: "/:realm/realm-settings",
component: RealmSettingsSection,
breadcrumb: t("realmSettings"),
access: "view-realm",
},
{
path: "/authentication",
path: "/:realm/authentication",
component: AuthenticationSection,
breadcrumb: t("authentication"),
access: "view-realm",
},
{
path: "/identity-providers",
path: "/:realm/identity-providers",
component: IdentityProvidersSection,
breadcrumb: t("identityProviders"),
access: "view-identity-providers",
},
{
path: "/user-federation",
path: "/:realm/user-federation",
component: UserFederationSection,
breadcrumb: t("userFederation"),
access: "view-realm",
},
{
path: "/user-federation/kerberos",
path: "/:realm/user-federation/kerberos",
component: UserFederationSection,
breadcrumb: null,
access: "view-realm",
},
{
path: "/user-federation/ldap",
path: "/:realm/user-federation/ldap",
component: UserFederationSection,
breadcrumb: null,
access: "view-realm",
},
{
path: "/user-federation/kerberos/:id",
path: "/:realm/user-federation/kerberos/:id",
component: UserFederationKerberosSettings,
breadcrumb: t("common:settings"),
access: "view-realm",
},
{
path: "/user-federation/ldap/:id",
path: "/:realm/user-federation/ldap/:id",
component: UserFederationLdapSettings,
breadcrumb: t("common:settings"),
access: "view-realm",
},
{
path: "/:realm/",
component: ClientsSection,
breadcrumb: t("common:home"),
access: "anyone",
},
{
path: "/",
component: ClientsSection,
@ -199,7 +204,7 @@ export const routes: RoutesFn = (t: any) => [
{
path: "*",
component: PageNotFoundSection,
breadcrumb: "",
breadcrumb: null,
access: "anyone",
},
];

View file

@ -19357,9 +19357,9 @@ use-latest@^1.0.0:
use-isomorphic-layout-effect "^1.0.0"
use-react-router-breadcrumbs@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/use-react-router-breadcrumbs/-/use-react-router-breadcrumbs-1.0.4.tgz#12b67ba27ac7e6a00e6ae10896ea91e178e87ee2"
integrity sha512-SskKm+wFYPD7eiYrg89y1Wn8vMlY+DiZXNFuP4Wt5gMP2aolcahHGR6pRTWsfMW93CEQxdVkXv/ceHL7nfz2Fw==
version "1.0.5"
resolved "https://registry.yarnpkg.com/use-react-router-breadcrumbs/-/use-react-router-breadcrumbs-1.0.5.tgz#3b39a2c2a6ab72544c2fc8984f6825d0f1122877"
integrity sha512-NDMgWr5MdksqnATRvp84RtZ0ABfuztlsgR4VWlsBV0D3TVV6xhbmkhTdV3cWnyRIZqNlMXZhwJhyRHoC6fbAsQ==
use-sidecar@^1.0.1:
version "1.0.3"