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-hook-form": "^6.8.2",
"react-i18next": "^11.7.0", "react-i18next": "^11.7.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"use-react-router-breadcrumbs": "^1.0.4" "use-react-router-breadcrumbs": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.5", "@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 { 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 { Header } from "./PageHeader";
import { PageNav } from "./PageNav"; import { PageNav } from "./PageNav";
@ -13,6 +18,7 @@ import { AccessContextProvider, useAccess } from "./context/access/Access";
import { routes, RouteDef } from "./route-config"; import { routes, RouteDef } from "./route-config";
import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs"; import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
import { ForbiddenSection } from "./ForbiddenSection"; import { ForbiddenSection } from "./ForbiddenSection";
import { useRealm } from "./context/realm-context/RealmContext";
// This must match the id given as scrollableSelector in scroll-form // This must match the id given as scrollableSelector in scroll-form
const mainPageContentId = "kc-main-content-page-container"; const mainPageContentId = "kc-main-content-page-container";
@ -31,6 +37,13 @@ const AppContexts = ({ children }: { children: ReactNode }) => (
// have access to, show forbidden page. // have access to, show forbidden page.
type SecuredRouteProps = { route: RouteDef }; type SecuredRouteProps = { route: RouteDef };
const SecuredRoute = ({ route }: SecuredRouteProps) => { const SecuredRoute = ({ route }: SecuredRouteProps) => {
const { setRealm } = useRealm();
const { realm } = useParams<{ realm: string }>();
useEffect(() => {
if (realm) {
setRealm(realm);
}
}, []);
const { hasAccess } = useAccess(); const { hasAccess } = useAccess();
if (hasAccess(route.access)) return <route.component />; if (hasAccess(route.access)) return <route.component />;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,20 +22,20 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
className="pf-c-breadcrumb__item" className="pf-c-breadcrumb__item"
> >
<Link <Link
to="/clients" to="/master"
> >
<LinkAnchor <LinkAnchor
href="/clients" href="/master"
navigate={[Function]} navigate={[Function]}
> >
<a <a
href="/clients" href="/master"
onClick={[Function]} onClick={[Function]}
> >
<span <span
key="/clients" key="/master"
> >
Clients Home
</span> </span>
</a> </a>
</LinkAnchor> </LinkAnchor>
@ -43,7 +43,7 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
</li> </li>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbItem <BreadcrumbItem
isActive={true} isActive={false}
key=".$1" key=".$1"
showDivider={true} showDivider={true}
> >
@ -78,8 +78,65 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
</svg> </svg>
</AngleRightIcon> </AngleRightIcon>
</span> </span>
<Link
to="/master/clients"
>
<LinkAnchor
href="/master/clients"
navigate={[Function]}
>
<a
href="/master/clients"
onClick={[Function]}
>
<span <span
key="/clients/1234" key="/master/clients"
>
Clients
</span>
</a>
</LinkAnchor>
</Link>
</li>
</BreadcrumbItem>
<BreadcrumbItem
isActive={true}
key=".$2"
showDivider={true}
>
<li
className="pf-c-breadcrumb__item"
>
<span
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 Client details
</span> </span>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,13 @@
import React, { useState, useContext } from "react"; import React, { useContext, useState } from "react";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
export const RealmContext = React.createContext({ type RealmContextType = {
realm: string;
setRealm: (realm: string) => void;
};
export const RealmContext = React.createContext<RealmContextType>({
realm: "", realm: "",
setRealm: (realm: string) => {}, setRealm: () => {},
}); });
type RealmContextProviderProps = { children: React.ReactNode }; type RealmContextProviderProps = { children: React.ReactNode };
@ -11,8 +15,7 @@ type RealmContextProviderProps = { children: React.ReactNode };
export const RealmContextProvider = ({ export const RealmContextProvider = ({
children, children,
}: RealmContextProviderProps) => { }: RealmContextProviderProps) => {
const homeRealm = useContext(WhoAmIContext).getHomeRealm(); const [realm, setRealm] = useState("");
const [realm, setRealm] = useState(homeRealm);
return ( return (
<RealmContext.Provider value={{ realm, setRealm }}> <RealmContext.Provider value={{ realm, setRealm }}>
@ -20,3 +23,5 @@ export const RealmContextProvider = ({
</RealmContext.Provider> </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 { DataLoader } from "../../components/data-loader/DataLoader";
import { AdminClient } from "../auth/AdminClient"; import { AdminClient } from "../auth/AdminClient";
import { RealmContext } from "../realm-context/RealmContext";
import WhoAmIRepresentation, { import WhoAmIRepresentation, {
AccessType, AccessType,
} from "keycloak-admin/lib/defs/whoAmIRepresentation"; } from "keycloak-admin/lib/defs/whoAmIRepresentation";
@ -54,17 +55,23 @@ export const WhoAmIContext = React.createContext(new WhoAmI());
type WhoAmIProviderProps = { children: React.ReactNode }; type WhoAmIProviderProps = { children: React.ReactNode };
export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => { export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
const adminClient = useContext(AdminClient)!; const adminClient = useContext(AdminClient)!;
const { realm, setRealm } = useContext(RealmContext);
const whoAmILoader = async () => { 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 ( return (
<DataLoader loader={whoAmILoader}> <DataLoader loader={whoAmILoader}>
{(whoamirep) => ( {(whoami) => (
<WhoAmIContext.Provider <WhoAmIContext.Provider value={whoami.data}>
value={new WhoAmI(adminClient.keycloak?.realm, whoamirep.data)}
>
{children} {children}
</WhoAmIContext.Provider> </WhoAmIContext.Provider>
)} )}

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import React, { useState } from "react"; 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 { useTranslation } from "react-i18next";
import { import {
AlertVariant, AlertVariant,
@ -23,6 +23,7 @@ export const RealmRolesSection = () => {
const history = useHistory(); const history = useHistory();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const { url } = useRouteMatch();
const [selectedRole, setSelectedRole] = useState<RoleRepresentation>(); const [selectedRole, setSelectedRole] = useState<RoleRepresentation>();
@ -37,7 +38,7 @@ export const RealmRolesSection = () => {
const RoleDetailLink = (role: RoleRepresentation) => ( const RoleDetailLink = (role: RoleRepresentation) => (
<> <>
<Link key={role.id} to={`/roles/${role.id}`}> <Link key={role.id} to={`${url}/${role.id}`}>
{role.name} {role.name}
</Link> </Link>
</> </>
@ -81,7 +82,7 @@ export const RealmRolesSection = () => {
}, },
}); });
const goToCreate = () => history.push("/roles/add-role"); const goToCreate = () => history.push(`${url}/add-role`);
return ( return (
<> <>
<ViewHeader titleKey="roles:title" subKey="roles:roleExplain" /> <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 { UserFederationKerberosSettings } from "./user-federation/UserFederationKerberosSettings";
import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings"; import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings";
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm"; import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
import { BreadcrumbsRoute } from "use-react-router-breadcrumbs";
export type RouteDef = { export type RouteDef = BreadcrumbsRoute & {
path: string;
component: () => JSX.Element; component: () => JSX.Element;
breadcrumb: TFunction | "";
access: AccessType; access: AccessType;
}; };
type RoutesFn = (t: TFunction) => RouteDef[]; 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, component: NewRealmForm,
breadcrumb: t("realm:createRealm"), breadcrumb: t("realm:createRealm"),
access: "manage-realm", access: "manage-realm",
}, },
{ {
path: "/clients/:id", path: "/:realm/clients",
component: ClientDetails,
breadcrumb: t("clients:clientSettings"),
access: "view-clients",
},
{
path: "/clients",
component: ClientsSection, component: ClientsSection,
breadcrumb: t("clients:clientList"), breadcrumb: t("clients:clientList"),
access: "query-clients", access: "query-clients",
}, },
{ {
path: "/add-client", path: "/:realm/clients/add-client",
component: NewClientForm, component: NewClientForm,
breadcrumb: t("clients:createClient"), breadcrumb: t("clients:createClient"),
access: "manage-clients", access: "manage-clients",
}, },
{ {
path: "/import-client", path: "/:realm/clients/import-client",
component: ImportForm, component: ImportForm,
breadcrumb: t("clients:importClient"), breadcrumb: t("clients:importClient"),
access: "manage-clients", 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, component: ClientScopeForm,
breadcrumb: t("client-scopes:createClientScope"), breadcrumb: t("client-scopes:createClientScope"),
access: "manage-clients", access: "manage-clients",
}, },
{ {
path: "/client-scopes/:id", path: "/:realm/client-scopes/:id",
component: ClientScopeForm, component: ClientScopeForm,
breadcrumb: t("client-scopes:clientScopeDetails"), breadcrumb: t("client-scopes:clientScopeDetails"),
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/client-scopes/:scopeId/oidc-role-name-mapper", path: "/:realm/client-scopes/:scopeId/oidc-role-name-mapper",
component: RoleMappingForm, component: RoleMappingForm,
breadcrumb: t("client-scopes:mappingDetails"), breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/client-scopes/:scopeId/:id", path: "/:realm/client-scopes/:scopeId/:id",
component: MappingDetails, component: MappingDetails,
breadcrumb: t("client-scopes:mappingDetails"), breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/client-scopes/:id", path: "/:realm/client-scopes/:id",
component: ClientScopeForm, component: ClientScopeForm,
breadcrumb: t("client-scopes:clientScopeDetails"), breadcrumb: t("client-scopes:clientScopeDetails"),
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/client-scopes", path: "/:realm/client-scopes",
component: ClientScopesSection, component: ClientScopesSection,
breadcrumb: t("client-scopes:clientScopeList"), breadcrumb: t("client-scopes:clientScopeList"),
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/roles", path: "/:realm/roles",
component: RealmRolesSection, component: RealmRolesSection,
breadcrumb: t("roles:roleList"), breadcrumb: t("roles:roleList"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/roles/add-role", path: "/:realm/roles/add-role",
component: RealmRolesForm, component: RealmRolesForm,
breadcrumb: t("roles:createRole"), breadcrumb: t("roles:createRole"),
access: "manage-realm", access: "manage-realm",
}, },
{ {
path: "/roles/:id", path: "/:realm/roles/:id",
component: RealmRolesForm, component: RealmRolesForm,
breadcrumb: t("roles:roleDetails"), breadcrumb: t("roles:roleDetails"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/users", path: "/:realm/users",
component: UsersSection, component: UsersSection,
breadcrumb: t("users:title"), breadcrumb: t("users:title"),
access: "query-users", access: "query-users",
}, },
{ {
path: "/groups", path: "/:realm/groups",
component: GroupsSection, component: GroupsSection,
breadcrumb: t("groups"), breadcrumb: t("groups"),
access: "query-groups", access: "query-groups",
}, },
{ {
path: "/sessions", path: "/:realm/sessions",
component: SessionsSection, component: SessionsSection,
breadcrumb: t("sessions:title"), breadcrumb: t("sessions:title"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/events", path: "/:realm/events",
component: EventsSection, component: EventsSection,
breadcrumb: t("events:title"), breadcrumb: t("events:title"),
access: "view-events", access: "view-events",
}, },
{ {
path: "/realm-settings", path: "/:realm/realm-settings",
component: RealmSettingsSection, component: RealmSettingsSection,
breadcrumb: t("realmSettings"), breadcrumb: t("realmSettings"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/authentication", path: "/:realm/authentication",
component: AuthenticationSection, component: AuthenticationSection,
breadcrumb: t("authentication"), breadcrumb: t("authentication"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/identity-providers", path: "/:realm/identity-providers",
component: IdentityProvidersSection, component: IdentityProvidersSection,
breadcrumb: t("identityProviders"), breadcrumb: t("identityProviders"),
access: "view-identity-providers", access: "view-identity-providers",
}, },
{ {
path: "/user-federation", path: "/:realm/user-federation",
component: UserFederationSection, component: UserFederationSection,
breadcrumb: t("userFederation"), breadcrumb: t("userFederation"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/user-federation/kerberos", path: "/:realm/user-federation/kerberos",
component: UserFederationSection, component: UserFederationSection,
breadcrumb: null, breadcrumb: null,
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/user-federation/ldap", path: "/:realm/user-federation/ldap",
component: UserFederationSection, component: UserFederationSection,
breadcrumb: null, breadcrumb: null,
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/user-federation/kerberos/:id", path: "/:realm/user-federation/kerberos/:id",
component: UserFederationKerberosSettings, component: UserFederationKerberosSettings,
breadcrumb: t("common:settings"), breadcrumb: t("common:settings"),
access: "view-realm", access: "view-realm",
}, },
{ {
path: "/user-federation/ldap/:id", path: "/:realm/user-federation/ldap/:id",
component: UserFederationLdapSettings, component: UserFederationLdapSettings,
breadcrumb: t("common:settings"), breadcrumb: t("common:settings"),
access: "view-realm", access: "view-realm",
}, },
{
path: "/:realm/",
component: ClientsSection,
breadcrumb: t("common:home"),
access: "anyone",
},
{ {
path: "/", path: "/",
component: ClientsSection, component: ClientsSection,
@ -199,7 +204,7 @@ export const routes: RoutesFn = (t: any) => [
{ {
path: "*", path: "*",
component: PageNotFoundSection, component: PageNotFoundSection,
breadcrumb: "", breadcrumb: null,
access: "anyone", access: "anyone",
}, },
]; ];

View file

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