8000 Use browser routing in admin UI and restore native JumpLinks behavior by edewit · Pull Request #51160 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

< 8000 label class="SelectMenu-item" role="menuitem"> Deleted files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions js/apps/admin-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<AdminClientProvider>
<AppContexts>
Expand Down
26 changes: 21 additions & 5 deletions js/apps/admin-ui/src/components/routable-tabs/RoutableTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
matchPath,
useHref,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
Expand Down Expand Up @@ -46,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] || [];
Expand Down Expand Up @@ -87,6 +89,16 @@ export const RoutableTabs = ({
activeKey={
exactMatch ?? nearestMatch ?? defaultLocation?.pathname ?? pathname
}
eventKey) => {
otherProps.onSelect?.(event, eventKey);

if (typeof eventKey !== "string" || eventKey === decodeURI(pathname)) {
return;
}

event.preventDefault();
navigate(eventKey);
}}
component={TabsComponent.nav}
inset={{
default: "insetNone",
Expand Down Expand Up @@ -120,13 +132,17 @@ const DynamicTab = ({
const href = useHref(props.eventKey);

return (
<Tab href={href} {...props}>
<Tab {...props} href={href}>
{children}
</Tab>
);
};

export const useRoutableTab = (to: Partial<Path>) => ({
eventKey: to.pathname ?? "",
href: useHref(to),
});
export const useRoutableTab = (to: Partial<Path>) => {
const href = useHref(to);

return {
eventKey: to.pathname ?? "",
href,
};
};
7 changes: 4 additions & 3 deletions js/apps/admin-ui/src/context/realm-context/RealmContext.tsx
8000
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<RealmRepresentation>();

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(() => {
Expand Down
25 changes: 0 additions & 25 deletions js/apps/admin-ui/src/context/realm-context/useHash.tsx

This file was deleted.

32 changes: 24 additions & 8 deletions js/apps/admin-ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,39 @@ 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";
import { joinPath } from "./utils/joinPath";

import "./index.css";

// Initialize required components before rendering app.
await i18n.init();

const router = createHashRouter([
{
path: "/",
element: <Root />,
children: routes,
},
]);
const basename =
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, "", joinPath(basename, hashPath));
}
Comment thread
edewit marked this conversation as resolved.
Resolved

const router = createBrowserRouter(
[
{
path: "/",
element: <Root />,
children: routes,
},
],
{ basename },
);
const container = document.getElementById("app");
const root = createRoot(container!);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,11 +67,7 @@ async function navigateBackAndVerifyClientScope(
testBed: Awaited<ReturnType<typeof createTestBed>>,
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 }));
await expect(page.getByPlaceholder("Search for client scope")).toBeVisible();

await page.getByPlaceholder("Search for client scope").fill(clientScopeName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Comment thread
edewit marked this conversation as resolved.
Resolved
}
});

Expand Down
15 changes: 12 additions & 3 deletions js/apps/admin-ui/test/identity-providers/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 3 additions & 10 deletions js/apps/admin-ui/test/realm-settings/oid4vci-attributes.spec.ts
Original file line number Diff line number Diff line change
@@ -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 ({
Expand Down Expand Up @@ -119,13 +117,8 @@ 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,
);
url.hash = toRealmSettings({ realm: testBed.realm }).pathname!;
await page.goto(url.toString());
// Navigate back to realm settings via browser routing.
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);
Expand Down
20 changes: 16 additions & 4 deletions js/apps/admin-ui/test/utils/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,25 @@ export async function navigateTo(
to: Partial<Path>,
rootRealm = DEFAULT_REALM,
): Promise<void> {
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());
Expand Down
4 changes: 2 additions & 2 deletions js/libs/ui-shared/src/scroll-form/FormPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export const FormPanel = ({
const id = useId();

return (
<Card id={id} className={className} isFlat>
<Card id={scrollId || id} className={className} isFlat tabIndex={-1}>
<CardHeader className="kc-form-panel__header">
<CardTitle tabIndex={0}>
<FormTitle id={scrollId} title={title} />
<FormTitle title={title} />
</CardTitle>
</CardHeader>
<CardBody className="kc-form-panel__body">{children}</CardBody>
Expand Down
48 changes: 10 additions & 38 deletions js/libs/ui-shared/src/scroll-form/ScrollForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 (
<Grid hasGutter {...rest}>
<GridItem md={8} sm={12}>
Expand Down Expand Up @@ -93,23 +68,20 @@ export const ScrollForm = ({
</GridItem>
<GridItem md={4} sm={12} order={{ default: "-1", md: "1" }}>
<PageSection className={style.sticky}>
<JumpLinks isVertical label={label}>
{shownSections.map(({ title }, index) => {
<JumpLinks
isVertical
scrollableSelector={`#${mainPageContentId}`}
label={label}
offset={100}
Comment on lines +73 to +75
>
{shownSections.map(({ title }) => {
const scrollId = spacesToHyphens(title.toLowerCase());

return (
<JumpLinksItem
key={title}
isActive={activeSection === index}
=> {
const element = document.getElementById(scrollId);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}}
href={`#${scrollId}`}
node={`#${scrollId}`}
data-testid={`jump-link-${scrollId}`}
Comment thread
edewit marked this conversation as resolved.
Resolved
>
{title}
Expand Down
Loading
Loading
0