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:
parent
27d9dadee7
commit
b14027ccb7
26 changed files with 697 additions and 589 deletions
|
@ -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",
|
||||
|
|
17
src/App.tsx
17
src/App.tsx
|
@ -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 />;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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={[
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue