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

Filter by extension

Filter by extension

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
}
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",
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
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}
onClick={() => {
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