From 67b3d7274e56a0cd4418892a8b1c436b3fb14607 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Sat, 25 Jul 2026 13:29:26 +0200 Subject: [PATCH 1/8] Use browser routing in admin UI and restore native JumpLinks behavior Switch the Admin Console from hash routing to browser routing with server-side deep-link fallback, and handle routable tab clicks through React Router so tab changes stay client-side. With path-based routing in place, remove the ScrollForm hash-safe click override and rely on PatternFly JumpLinks href/node behavior, including stable anchors for bordered panels. Closes #39693 Closes #45494 Related: patternfly/patternfly-react#12223 Signed-off-by: Erik Jan de Wit --- js/apps/admin-ui/src/App.tsx | 8 +--- .../components/routable-tabs/RoutableTabs.tsx | 19 ++++++-- .../context/realm-context/RealmContext.tsx | 7 +-- .../src/context/realm-context/useHash.tsx | 25 ---------- js/apps/admin-ui/src/main.tsx | 30 ++++++++---- .../client-scope/oid4vci-client-scope.spec.ts | 10 ++-- .../identity-providers/default-trust.spec.ts | 6 ++- .../admin-ui/test/identity-providers/main.ts | 2 +- .../realm-settings/oid4vci-attributes.spec.ts | 15 +++--- js/apps/admin-ui/test/utils/login.ts | 20 ++++++-- .../ui-shared/src/scroll-form/FormPanel.tsx | 4 +- .../ui-shared/src/scroll-form/ScrollForm.tsx | 48 ++++--------------- .../resources/admin/AdminConsole.java | 8 ++-- 13 files changed, 91 insertions(+), 111 deletions(-) delete mode 100644 js/apps/admin-ui/src/context/realm-context/useHash.tsx diff --git a/js/apps/admin-ui/src/App.tsx b/js/apps/admin-ui/src/App.tsx index 7dc43b3c0b36..5e6d30ee2ca0 100644 --- a/js/apps/admin-ui/src/App.tsx +++ b/js/apps/admin-ui/src/App.tsx @@ -5,7 +5,7 @@ import { mainPageContentId, } from "@keycloak/keycloak-ui-shared"; import { Flex, FlexItem, Page } from "@patternfly/react-core"; -import { PropsWithChildren, Suspense, useEffect } from "react"; +import { PropsWithChildren, Suspense } from "react"; import { Outlet } from "react-router-dom"; import { AdminClientProvider } from "./admin-client"; import { Header } from "./PageHeader"; @@ -40,12 +40,6 @@ export const AppContexts = ({ children }: PropsWithChildren) => ( ); export const App = () => { - const hrefEndsWithHashSlash = location.href.endsWith("#/"); - useEffect(() => { - if (!hrefEndsWithHashSlash) return; - history.replaceState(null, "", location.pathname); - }, [hrefEndsWithHashSlash, location.pathname]); - return ( diff --git a/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx b/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx index 8a2987826d91..c0c34bd5d06f 100644 --- a/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx +++ b/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx @@ -14,9 +14,11 @@ import { } from "react"; import { Path, + To, generatePath, matchPath, useHref, + useLinkClickHandler, useLocation, useParams, } from "react-router-dom"; @@ -118,15 +120,22 @@ const DynamicTab = ({ ...props }: PropsWithChildren) => { const href = useHref(props.eventKey); + const onClick = useLinkClickHandler(props.eventKey); return ( - + {children} ); }; -export const useRoutableTab = (to: Partial) => ({ - eventKey: to.pathname ?? "", - href: useHref(to), -}); +export const useRoutableTab = (to: Partial) => { + const href = useHref(to); + const onClick = useLinkClickHandler(to as To); + + return { + eventKey: to.pathname ?? "", + href, + onClick, + }; +}; diff --git a/js/apps/admin-ui/src/context/realm-context/RealmContext.tsx b/js/apps/admin-ui/src/context/realm-context/RealmContext.tsx index a0f89dfb8cc4..7f89da0b66cd 100644 --- a/js/apps/admin-ui/src/context/realm-context/RealmContext.tsx +++ b/js/apps/admin-ui/src/context/realm-context/RealmContext.tsx @@ -7,8 +7,8 @@ import { useRequiredContext, } from "@keycloak/keycloak-ui-shared"; import { PropsWithChildren, useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; import { useAdminClient } from "../../admin-client"; -import { useHash } from "./useHash"; import { useTranslation } from "react-i18next"; type RealmContextType = { @@ -26,13 +26,14 @@ export const RealmContextProvider = ({ children }: PropsWithChildren) => { const { adminClient } = useAdminClient(); const { environment } = useEnvironment(); const { i18n } = useTranslation(); + const { pathname } = useLocation(); const [key, setKey] = useState(0); const refresh = () => setKey(key + 1); const [realmRepresentation, setRealmRepresentation] = useState(); - const locationRealm = useHash(); - const realm = locationRealm.split("/")[1] ?? environment.realm; + const locationRealm = decodeURIComponent(pathname); + const realm = locationRealm.split("/")[1] || environment.realm; // Configure admin client to use selected realm when it changes. useEffect(() => { diff --git a/js/apps/admin-ui/src/context/realm-context/useHash.tsx b/js/apps/admin-ui/src/context/realm-context/useHash.tsx deleted file mode 100644 index 1fa12c6b23e3..000000000000 --- a/js/apps/admin-ui/src/context/realm-context/useHash.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useEffect, useState } from "react"; - -function getHash(url: URL) { - return decodeURIComponent(url.hash.substring(1)); -} - -export const useHash = () => { - const [hash, setHash] = useState(getHash(new URL(window.location.href))); - - useEffect(() => { - const orgPushState = window.history.pushState; - window.history.pushState = new Proxy(window.history.pushState, { - apply: (func, target, args) => { - const url = new URL(args[2], window.location.origin); - setHash(getHash(url)); - return Reflect.apply(func, target, args); - }, - }); - return () => { - window.history.pushState = orgPushState; - }; - }, []); - - return hash; -}; diff --git a/js/apps/admin-ui/src/main.tsx b/js/apps/admin-ui/src/main.tsx index 119ab93ef950..6e28b3e19d3e 100644 --- a/js/apps/admin-ui/src/main.tsx +++ b/js/apps/admin-ui/src/main.tsx @@ -3,7 +3,8 @@ import "@patternfly/react-core/dist/styles/base.css"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { createHashRouter, RouterProvider } from "react-router-dom"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { environment } from "./environment"; import { i18n } from "./i18n/i18n"; import { Root } from "./Root"; import { routes } from "./routes"; @@ -13,13 +14,26 @@ import "./index.css"; // Initialize required components before rendering app. await i18n.init(); -const router = createHashRouter([ - { - path: "/", - element: , - children: routes, - }, -]); +const basename = + decodeURIComponent( + new URL(environment.consoleBaseUrl, window.location.origin).pathname, + ).replace(/\/+$/, "") || "/"; + +if (window.location.hash.startsWith("#/")) { + const hashPath = decodeURIComponent(window.location.hash.substring(1)); + window.history.replaceState(null, "", `${basename}${hashPath}`); +} + +const router = createBrowserRouter( + [ + { + path: "/", + element: , + children: routes, + }, + ], + { basename }, +); const container = document.getElementById("app"); const root = createRoot(container!); diff --git a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts index cba69d2d86f4..599243f77047 100644 --- a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts +++ b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts @@ -10,7 +10,7 @@ import { assertSaveButtonIsDisabled, } from "../utils/form.ts"; import { clickTableRowItem, clickTableToolbarItem } from "../utils/table.ts"; -import { login } from "../utils/login.ts"; +import { login, navigateTo } from "../utils/login.ts"; import { OID4VCI_PROTOCOL, skipIfOID4VCIFeatureDisabled, @@ -67,10 +67,10 @@ async function navigateBackAndVerifyClientScope( testBed: Awaited>, clientScopeName: string, ) { - const currentUrl = page.url(); - const baseUrl = currentUrl.split("#")[0]; - await page.goto( - `${baseUrl}#${toClientScopes({ realm: testBed.realm }).pathname!}`, + await navigateTo( + page, + toClientScopes({ realm: testBed.realm }), + testBed.realm, ); await expect(page.getByPlaceholder("Search for client scope")).toBeVisible(); diff --git a/js/apps/admin-ui/test/identity-providers/default-trust.spec.ts b/js/apps/admin-ui/test/identity-providers/default-trust.spec.ts index fc8b885ce7e7..d5683cdfe508 100644 --- a/js/apps/admin-ui/test/identity-providers/default-trust.spec.ts +++ b/js/apps/admin-ui/test/identity-providers/default-trust.spec.ts @@ -26,7 +26,11 @@ test.describe.serial("Default Trust identity provider test", () => { test.afterEach(async ({}, testInfo) => { if (testInfo.title.includes("create and edit")) { - await adminClient.deleteIdentityProvider(alias); + try { + await adminClient.deleteIdentityProvider(alias); + } catch { + // The provider may already be deleted when creation fails. + } } }); diff --git a/js/apps/admin-ui/test/identity-providers/main.ts b/js/apps/admin-ui/test/identity-providers/main.ts index b91ca6feedfb..6f69a2500769 100644 --- a/js/apps/admin-ui/test/identity-providers/main.ts +++ b/js/apps/admin-ui/test/identity-providers/main.ts @@ -97,7 +97,7 @@ export async function createDefaultTrustProvider( (await page.getByTestId("currentRealm").textContent()) ?? "master"; await page.goto( - `${SERVER_URL}/admin/master/console/#/${realm}/identity-providers/default-trust/add`, + `${SERVER_URL}/admin/master/console/${realm}/identity-providers/default-trust/add`, ); await page.getByTestId("alias").fill(alias); diff --git a/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts b/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts index 0787e28f7c4b..823655b80658 100644 --- a/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts +++ b/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts @@ -1,10 +1,8 @@ import { expect, test } from "@playwright/test"; -import { generatePath } from "react-router-dom"; import { toRealmSettings } from "../../src/realm-settings/routes/RealmSettings.tsx"; import { createTestBed } from "../support/testbed.ts"; import adminClient from "../utils/AdminClient.js"; -import { SERVER_URL, ROOT_PATH } from "../utils/constants.ts"; -import { login } from "../utils/login.js"; +import { login, navigateTo } from "../utils/login.js"; import { changeTimeUnit, selectItem } from "../utils/form.ts"; test("OID4VCI section is hidden in Tokens tab when verifiable credentials are disabled", async ({ @@ -119,13 +117,12 @@ test("should persist values after page refresh", async ({ page }) => { // Refresh the page await page.reload(); - // Navigate back to realm settings using the same pattern as login - const url = new URL( - generatePath(ROOT_PATH, { realm: testBed.realm }), - SERVER_URL, + // Navigate back to realm settings via browser routing. + await navigateTo( + page, + toRealmSettings({ realm: testBed.realm }), + testBed.realm, ); - url.hash = toRealmSettings({ realm: testBed.realm }).pathname!; - await page.goto(url.toString()); // The TimeSelector component converts values based on units, so we need to check the actual saved values const realmData = await adminClient.getRealm(testBed.realm); diff --git a/js/apps/admin-ui/test/utils/login.ts b/js/apps/admin-ui/test/utils/login.ts index 5153084364b2..ccc4156e0fd1 100644 --- a/js/apps/admin-ui/test/utils/login.ts +++ b/js/apps/admin-ui/test/utils/login.ts @@ -43,13 +43,25 @@ export async function navigateTo( to: Partial, rootRealm = DEFAULT_REALM, ): Promise { - const url = new URL( - generatePath(ROOT_PATH, { realm: rootRealm }), - SERVER_URL, + const rootPath = generatePath(ROOT_PATH, { realm: rootRealm }).replace( + /\/+$/, + "", ); + const url = new URL(rootPath, SERVER_URL); if (to.pathname) { - url.hash = to.pathname; + const destinationPath = to.pathname.startsWith("/") + ? to.pathname + : `/${to.pathname}`; + url.pathname = `${rootPath}${destinationPath}`; + } + + if (to.search) { + url.search = to.search; + } + + if (to.hash) { + url.hash = to.hash; } await page.goto(url.toString()); diff --git a/js/libs/ui-shared/src/scroll-form/FormPanel.tsx b/js/libs/ui-shared/src/scroll-form/FormPanel.tsx index 50e4e7fe382f..b18f22bdae13 100644 --- a/js/libs/ui-shared/src/scroll-form/FormPanel.tsx +++ b/js/libs/ui-shared/src/scroll-form/FormPanel.tsx @@ -17,10 +17,10 @@ export const FormPanel = ({ const id = useId(); return ( - + - + {children} diff --git a/js/libs/ui-shared/src/scroll-form/ScrollForm.tsx b/js/libs/ui-shared/src/scroll-form/ScrollForm.tsx index c9de609e8af4..685d0c2add46 100644 --- a/js/libs/ui-shared/src/scroll-form/ScrollForm.tsx +++ b/js/libs/ui-shared/src/scroll-form/ScrollForm.tsx @@ -6,7 +6,7 @@ import { JumpLinksItem, PageSection, } from "@patternfly/react-core"; -import { Fragment, ReactNode, useEffect, useMemo, useState } from "react"; +import { Fragment, ReactNode, useMemo } from "react"; import { FormPanel } from "./FormPanel"; import { ScrollPanel } from "./ScrollPanel"; @@ -41,31 +41,6 @@ export const ScrollForm = ({ [sections], ); - const [activeSection, setActiveSection] = useState(0); - - useEffect(() => { - const scroller = document.getElementById(mainPageContentId); - if (!scroller) return; - - const offset = 100; - const updateActive = () => { - const scrollTop = scroller.scrollTop + offset; - let active = 0; - shownSections.forEach(({ title }, index) => { - const id = spacesToHyphens(title.toLowerCase()); - const el = document.getElementById(id); - if (el && scrollTop >= el.offsetTop) { - active = index; - } - }); - setActiveSection(active); - }; - - updateActive(); - scroller.addEventListener("scroll", updateActive); - return () => scroller.removeEventListener("scroll", updateActive); - }, [shownSections]); - return ( @@ -93,23 +68,20 @@ export const ScrollForm = ({ - - {shownSections.map(({ title }, index) => { + + {shownSections.map(({ title }) => { const scrollId = spacesToHyphens(title.toLowerCase()); return ( { - const element = document.getElementById(scrollId); - if (element) { - element.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - } - }} + href={`#${scrollId}`} + node={`#${scrollId}`} data-testid={`jump-link-${scrollId}`} > {title} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index 8be3ae16e058..2c89e03a0589 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -31,6 +31,7 @@ import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; @@ -323,12 +324,13 @@ protected RealmModel getAdminstrationRealm(RealmManager realmManager) { */ @GET @NoCache - public Response getMainPage() throws IOException, FreeMarkerException { + @Path("{path:.*}") + public Response getMainPage(@PathParam("path") String path) throws IOException, FreeMarkerException { final var baseUriInfo = session.getContext().getUri(UrlType.FRONTEND); final var adminUriInfo = session.getContext().getUri(UrlType.ADMIN); - // Redirect to a URL with a trailing slash if the current URL doesn't have one. - if (!adminUriInfo.getRequestUri().getPath().endsWith("/")) { + // Redirect root URL to one with a trailing slash. + if ((path == null || path.isBlank()) && !adminUriInfo.getRequestUri().getPath().endsWith("/")) { return Response.status(302).location(adminUriInfo.getRequestUriBuilder().path("/").build()).build(); } else { // Get the base URLs of the server and admin console. From 601fdbb74e56c02bb0d70724814e1603611af179 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Tue, 28 Jul 2026 13:52:11 +0200 Subject: [PATCH 2/8] fixed navigation in test Signed-off-by: Erik Jan de Wit --- .../admin-ui/test/client-scope/oid4vci-client-scope.spec.ts | 6 +----- .../admin-ui/test/realm-settings/oid4vci-attributes.spec.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts index 599243f77047..65c645afb92d 100644 --- a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts +++ b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts @@ -67,11 +67,7 @@ async function navigateBackAndVerifyClientScope( testBed: Awaited>, clientScopeName: string, ) { - await navigateTo( - page, - toClientScopes({ realm: testBed.realm }), - testBed.realm, - ); + await navigateTo(page, toClientScopes({ realm: testBed.realm })); await expect(page.getByPlaceholder("Search for client scope")).toBeVisible(); await page.getByPlaceholder("Search for client scope").fill(clientScopeName); diff --git a/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts b/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts index 823655b80658..1090a7ca60f8 100644 --- a/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts +++ b/js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts @@ -118,11 +118,7 @@ test("should persist values after page refresh", async ({ page }) => { await page.reload(); // Navigate back to realm settings via browser routing. - await navigateTo( - page, - toRealmSettings({ realm: testBed.realm }), - testBed.realm, - ); + await navigateTo(page, toRealmSettings({ realm: testBed.realm })); // The TimeSelector component converts values based on units, so we need to check the actual saved values const realmData = await adminClient.getRealm(testBed.realm); From b81fd04585c8d3551bebd45e9b115cb9ebac0637 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Tue, 28 Jul 2026 14:02:43 +0200 Subject: [PATCH 3/8] PR review Signed-off-by: Erik Jan de Wit --- js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx | 2 +- js/apps/admin-ui/src/main.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx b/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx index c0c34bd5d06f..6abcdfd52250 100644 --- a/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx +++ b/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx @@ -123,7 +123,7 @@ const DynamicTab = ({ const onClick = useLinkClickHandler(props.eventKey); return ( - + {children} ); diff --git a/js/apps/admin-ui/src/main.tsx b/js/apps/admin-ui/src/main.tsx index 6e28b3e19d3e..bb62d9e499e8 100644 --- a/js/apps/admin-ui/src/main.tsx +++ b/js/apps/admin-ui/src/main.tsx @@ -8,6 +8,7 @@ import { environment } from "./environment"; import { i18n } from "./i18n/i18n"; import { Root } from "./Root"; import { routes } from "./routes"; +import { joinPath } from "./utils/joinPath"; import "./index.css"; @@ -21,7 +22,7 @@ const basename = if (window.location.hash.startsWith("#/")) { const hashPath = decodeURIComponent(window.location.hash.substring(1)); - window.history.replaceState(null, "", `${basename}${hashPath}`); + window.history.replaceState(null, "", joinPath(basename, hashPath)); } const router = createBrowserRouter( From 0c650d51c00506457fb8b9ac84785ae8af504f37 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Wed, 29 Jul 2026 11:20:32 +0200 Subject: [PATCH 4/8] fix routable tabs Signed-off-by: Erik Jan de Wit --- .../components/routable-tabs/RoutableTabs.tsx | 19 +++++++++++++------ .../admin-ui/test/identity-providers/main.ts | 7 +------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx b/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx index 6abcdfd52250..ddca4da421fc 100644 --- a/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx +++ b/js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx @@ -14,12 +14,11 @@ import { } from "react"; import { Path, - To, generatePath, matchPath, useHref, - useLinkClickHandler, useLocation, + useNavigate, useParams, } from "react-router-dom"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; @@ -48,6 +47,7 @@ export const RoutableTabs = ({ ...otherProps }: RoutableTabsProps) => { const { pathname } = useLocation(); + const navigate = useNavigate(); const params = useParams(); const { componentTypes } = useServerInfo(); const tabs = componentTypes?.[TAB_PROVIDER] || []; @@ -89,6 +89,16 @@ export const RoutableTabs = ({ activeKey={ exactMatch ?? nearestMatch ?? defaultLocation?.pathname ?? pathname } + onSelect={(event, eventKey) => { + otherProps.onSelect?.(event, eventKey); + + if (typeof eventKey !== "string" || eventKey === decodeURI(pathname)) { + return; + } + + event.preventDefault(); + navigate(eventKey); + }} component={TabsComponent.nav} inset={{ default: "insetNone", @@ -120,10 +130,9 @@ const DynamicTab = ({ ...props }: PropsWithChildren) => { const href = useHref(props.eventKey); - const onClick = useLinkClickHandler(props.eventKey); return ( - + {children} ); @@ -131,11 +140,9 @@ const DynamicTab = ({ export const useRoutableTab = (to: Partial) => { const href = useHref(to); - const onClick = useLinkClickHandler(to as To); return { eventKey: to.pathname ?? "", href, - onClick, }; }; diff --git a/js/apps/admin-ui/test/identity-providers/main.ts b/js/apps/admin-ui/test/identity-providers/main.ts index 6f69a2500769..24484cbbba51 100644 --- a/js/apps/admin-ui/test/identity-providers/main.ts +++ b/js/apps/admin-ui/test/identity-providers/main.ts @@ -93,12 +93,7 @@ export async function createDefaultTrustProvider( alias: string, jwksUrl: string, ) { - const realm = - (await page.getByTestId("currentRealm").textContent()) ?? "master"; - - await page.goto( - `${SERVER_URL}/admin/master/console/${realm}/identity-providers/default-trust/add`, - ); + await clickProviderCard(page, "default-trust"); await page.getByTestId("alias").fill(alias); await expect(page.getByTestId("config.useJwksUrl")).toBeChecked(); From f2faca560901d98ff548ad442fe2b8d3d81f39ec Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 30 Jul 2026 10:36:44 +0200 Subject: [PATCH 5/8] keep base url Signed-off-by: Erik Jan de Wit --- js/apps/admin-ui/src/main.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/apps/admin-ui/src/main.tsx b/js/apps/admin-ui/src/main.tsx index bb62d9e499e8..7259440c3500 100644 --- a/js/apps/admin-ui/src/main.tsx +++ b/js/apps/admin-ui/src/main.tsx @@ -16,9 +16,10 @@ import "./index.css"; await i18n.init(); const basename = - decodeURIComponent( - new URL(environment.consoleBaseUrl, window.location.origin).pathname, - ).replace(/\/+$/, "") || "/"; + new URL(environment.consoleBaseUrl, window.location.origin).pathname.replace( + /\/+$/, + "", + ) || "/"; if (window.location.hash.startsWith("#/")) { const hashPath = decodeURIComponent(window.location.hash.substring(1)); From 559cb94f7be06e218b8397be99981c403e1ec38c Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 30 Jul 2026 11:47:28 +0200 Subject: [PATCH 6/8] fix test Signed-off-by: Erik Jan de Wit --- js/apps/admin-ui/test/identity-providers/main.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/apps/admin-ui/test/identity-providers/main.ts b/js/apps/admin-ui/test/identity-providers/main.ts index 24484cbbba51..6f69a2500769 100644 --- a/js/apps/admin-ui/test/identity-providers/main.ts +++ b/js/apps/admin-ui/test/identity-providers/main.ts @@ -93,7 +93,12 @@ export async function createDefaultTrustProvider( alias: string, jwksUrl: string, ) { - await clickProviderCard(page, "default-trust"); + const realm = + (await page.getByTestId("currentRealm").textContent()) ?? "master"; + + await page.goto( + `${SERVER_URL}/admin/master/console/${realm}/identity-providers/default-trust/add`, + ); await page.getByTestId("alias").fill(alias); await expect(page.getByTestId("config.useJwksUrl")).toBeChecked(); From f4e8df20584541a3e75b0cbb519dc0d6b2a23c5f Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Fri, 31 Jul 2026 12:19:05 +0200 Subject: [PATCH 7/8] fixed test Signed-off-by: Erik Jan de Wit --- js/apps/admin-ui/test/identity-providers/main.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/js/apps/admin-ui/test/identity-providers/main.ts b/js/apps/admin-ui/test/identity-providers/main.ts index 6f69a2500769..5c6a65ffe6ed 100644 --- a/js/apps/admin-ui/test/identity-providers/main.ts +++ b/js/apps/admin-ui/test/identity-providers/main.ts @@ -88,16 +88,25 @@ export async function createJwtAuthorizationGrantProviderKey( await clickAddButton(page); } +function getCurrentAdminConsoleRealms(page: Page) { + const { pathname } = new URL(page.url()); + const match = /^\/admin\/([^/]+)\/console\/([^/]+)/.exec(pathname); + + return { + rootRealm: match?.[1] ?? "master", + realm: match?.[2] ?? "master", + }; +} + export async function createDefaultTrustProvider( page: Page, alias: string, jwksUrl: string, ) { - const realm = - (await page.getByTestId("currentRealm").textContent()) ?? "master"; + const { rootRealm, realm } = getCurrentAdminConsoleRealms(page); await page.goto( - `${SERVER_URL}/admin/master/console/${realm}/identity-providers/default-trust/add`, + `${SERVER_URL}/admin/${rootRealm}/console/${realm}/identity-providers/default-trust/add`, ); await page.getByTestId("alias").fill(alias); From 54c02212e5967feeae9272671b226feb5f7fb68b Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Tue, 18 Aug 2026 12:15:23 +0200 Subject: [PATCH 8/8] fixed path Signed-off-by: Erik Jan de Wit --- .../keycloak/services/resources/admin/AdminConsole.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index 2c89e03a0589..9d66afb94085 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -322,10 +322,16 @@ protected RealmModel getAdminstrationRealm(RealmManager realmManager) { /** * Main page of this realm's admin console. */ + @GET + @NoCache + public Response getMainPage() throws IOException, FreeMarkerException { + return getMainPageForPath(""); + } + @GET @NoCache @Path("{path:.*}") - public Response getMainPage(@PathParam("path") String path) throws IOException, FreeMarkerException { + public Response getMainPageForPath(@PathParam("path") String path) throws IOException, FreeMarkerException { final var baseUriInfo = session.getContext().getUri(UrlType.FRONTEND); final var adminUriInfo = session.getContext().getUri(UrlType.ADMIN);