diff --git a/js/apps/admin-ui/src/App.tsx b/js/apps/admin-ui/src/App.tsx
index af0202c3fa9..62840fdacb5 100644
--- a/js/apps/admin-ui/src/App.tsx
+++ b/js/apps/admin-ui/src/App.tsx
@@ -25,9 +25,9 @@ import { AuthWall } from "./root/AuthWall";
const AppContexts = ({ children }: PropsWithChildren) => (
-
-
-
+
+
+
@@ -37,9 +37,9 @@ const AppContexts = ({ children }: PropsWithChildren) => (
-
-
-
+
+
+
);
diff --git a/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx b/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx
index 545e314eb6d..63314df76cf 100644
--- a/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx
+++ b/js/apps/admin-ui/src/components/realm-selector/RealmSelector.tsx
@@ -11,11 +11,14 @@ import {
Spinner,
Split,
SplitItem,
+ Stack,
+ StackItem,
} from "@patternfly/react-core";
import { CheckIcon } from "@patternfly/react-icons";
import { Fragment, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link, To, useHref } from "react-router-dom";
+import { label } from "ui-shared";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useRealms } from "../../context/RealmsContext";
@@ -47,16 +50,33 @@ const AddRealm = ({ onClick }: AddRealmProps) => {
};
type RealmTextProps = {
- value: string;
+ name: string;
+ displayName?: string;
+ showIsRecent?: boolean;
};
-const RealmText = ({ value }: RealmTextProps) => {
+const RealmText = ({ name, displayName, showIsRecent }: RealmTextProps) => {
const { realm } = useRealm();
+ const { t } = useTranslation();
return (
- {value}
- {value === realm && }
+
+
+ {displayName ? (
+
+ {label(t, displayName)}
+
+ ) : null}
+ {name}
+
+
+ {name === realm && }
+ {showIsRecent ? (
+
+
+
+ ) : null}
);
};
@@ -80,7 +100,7 @@ const ContextSelectorItemLink = ({
export const RealmSelector = () => {
const { realm } = useRealm();
- const { realms, refresh } = useRealms();
+ const { realms } = useRealms();
const { whoAmI } = useWhoAmI();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
@@ -89,35 +109,50 @@ export const RealmSelector = () => {
const all = useMemo(
() =>
- recentRealms
- .filter((r) => r !== realm)
- .map((name) => {
- return { name, used: true };
+ realms
+ .filter((r) => r.name !== realm)
+ .map((realm) => {
+ const used = recentRealms.some((name) => name === realm.name);
+ return { realm, used };
})
- .concat(
- realms
- .filter((name) => !recentRealms.includes(name) || name === realm)
- .map((name) => ({ name, used: false })),
- ),
+ .sort((r1, r2) => {
+ if (r1.used == r2.used) return 0;
+ if (r1.used) return -1;
+ if (r2.used) return 1;
+ return 0;
+ }),
[recentRealms, realm, realms],
);
- const filteredItems = useMemo(
- () =>
- search.trim() === ""
- ? all
- : all.filter((r) =>
- r.name.toLowerCase().includes(search.toLowerCase()),
- ),
- [search, all],
+ const filteredItems = useMemo(() => {
+ const normalizedSearch = search.trim().toLowerCase();
+
+ if (normalizedSearch.length === 0) {
+ return all;
+ }
+
+ return search.trim() === ""
+ ? all
+ : all.filter(
+ (r) =>
+ r.realm.name.toLowerCase().includes(normalizedSearch) ||
+ label(t, r.realm.displayName)
+ ?.toLowerCase()
+ .includes(normalizedSearch),
+ );
+ }, [search, all]);
+
+ const realmDisplayName = useMemo(
+ () => realms.find((r) => r.name === realm)?.displayName,
+ [realm, realms],
);
return realms.length > 5 ? (
setOpen(!open)}
searchInputValue={search}
onSearchInputChange={(value) => setSearch(value)}
@@ -132,12 +167,11 @@ export const RealmSelector = () => {
>
{filteredItems.map((item) => (
setOpen(false)}
>
- {" "}
- {item.used && }
+ {" "}
))}
@@ -151,24 +185,23 @@ export const RealmSelector = () => {
{
- if (realms.length === 0) refresh();
setOpen(!open);
}}
className="keycloak__realm_selector_dropdown__toggle"
>
- {realm}
+ {label(t, realmDisplayName, realm)}
}
dropdownItems={(realms.length !== 0
- ? realms.map((name) => (
+ ? realms.map((realm) => (
setOpen(false)}
>
-
+
}
/>
diff --git a/js/apps/admin-ui/src/context/RealmsContext.tsx b/js/apps/admin-ui/src/context/RealmsContext.tsx
index b9ac9e3c408..ed84f25c277 100644
--- a/js/apps/admin-ui/src/context/RealmsContext.tsx
+++ b/js/apps/admin-ui/src/context/RealmsContext.tsx
@@ -1,40 +1,47 @@
import { NetworkError } from "@keycloak/keycloak-admin-client";
import { PropsWithChildren, useCallback, useMemo, useState } from "react";
-import { createNamedContext, useRequiredContext } from "ui-shared";
+import { createNamedContext, useRequiredContext, label } from "ui-shared";
import { keycloak } from "../keycloak";
import { useFetch } from "../utils/useFetch";
import { fetchAdminUI } from "./auth/admin-ui-endpoint";
+import useLocaleSort from "../utils/useLocaleSort";
+import { useTranslation } from "react-i18next";
type RealmsContextProps = {
/** A list of all the realms. */
- realms: string[];
+ realms: RealmNameRepresentation[];
/** Refreshes the realms with the latest information. */
refresh: () => Promise;
};
+export interface RealmNameRepresentation {
+ name: string;
+ displayName?: string;
+}
+
export const RealmsContext = createNamedContext(
"RealmsContext",
undefined,
);
export const RealmsProvider = ({ children }: PropsWithChildren) => {
- const [realms, setRealms] = useState([]);
+ const [realms, setRealms] = useState([]);
const [refreshCount, setRefreshCount] = useState(0);
+ const localeSort = useLocaleSort();
+ const { t } = useTranslation();
- function updateRealms(realms: string[]) {
- setRealms(realms.sort());
+ function updateRealms(realms: RealmNameRepresentation[]) {
+ setRealms(localeSort(realms, (r) => label(t, r.displayName, r.name)));
}
useFetch(
async () => {
- // We don't want to fetch until the user has requested it, so let's ignore the initial mount.
- if (refreshCount === 0) {
- return [];
- }
-
try {
- return await fetchAdminUI("ui-ext/realms/names", {});
+ return await fetchAdminUI(
+ "ui-ext/realms/names",
+ {},
+ );
} catch (error) {
if (error instanceof NetworkError && error.response.status < 500) {
return [];
diff --git a/js/apps/admin-ui/src/context/RecentRealms.tsx b/js/apps/admin-ui/src/context/RecentRealms.tsx
index 9d929405ad9..1cde5d88945 100644
--- a/js/apps/admin-ui/src/context/RecentRealms.tsx
+++ b/js/apps/admin-ui/src/context/RecentRealms.tsx
@@ -6,7 +6,7 @@ import {
useStoredState,
} from "ui-shared";
import { useRealm } from "./realm-context/RealmContext";
-import { useRealms } from "./RealmsContext";
+import { RealmNameRepresentation, useRealms } from "./RealmsContext";
const MAX_REALMS = 4;
@@ -43,12 +43,17 @@ export const RecentRealmsProvider = ({ children }: PropsWithChildren) => {
export const useRecentRealms = () => useRequiredContext(RecentRealmsContext);
-function filterRealmNames(realms: string[], storedRealms: string[]) {
+function filterRealmNames(
+ realms: RealmNameRepresentation[],
+ storedRealms: string[],
+) {
// If no realms have been set yet we can't filter out any non-existent realm names.
if (realms.length === 0) {
return storedRealms;
}
// Only keep realm names that actually still exist.
- return storedRealms.filter((realm) => realms.includes(realm));
+ return storedRealms.filter((realm) => {
+ return realms.some((r) => r.name === realm);
+ });
}
diff --git a/js/apps/admin-ui/src/dashboard/Dashboard.tsx b/js/apps/admin-ui/src/dashboard/Dashboard.tsx
index 5a6e0d72218..7767d16bd8a 100644
--- a/js/apps/admin-ui/src/dashboard/Dashboard.tsx
+++ b/js/apps/admin-ui/src/dashboard/Dashboard.tsx
@@ -34,7 +34,7 @@ import FeatureRepresentation, {
} from "@keycloak/keycloak-admin-client/lib/defs/featureRepresentation";
import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
-import { HelpItem } from "ui-shared";
+import { HelpItem, label } from "ui-shared";
import environment from "../environment";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
@@ -57,7 +57,7 @@ const EmptyDashboard = () => {
const [realmInfo, setRealmInfo] = useState();
const brandImage = environment.logo ? environment.logo : "/icon.svg";
useFetch(() => adminClient.realms.findOne({ realm }), setRealmInfo, []);
- const realmDisplayInfo = realmInfo?.displayName || realm;
+ const realmDisplayInfo = label(t, realmInfo?.displayName, realm);
return (
@@ -133,7 +133,7 @@ const Dashboard = () => {
useFetch(() => adminClient.realms.findOne({ realm }), setRealmInfo, []);
- const realmDisplayInfo = realmInfo?.displayName || realm;
+ const realmDisplayInfo = label(t, realmInfo?.displayName, realm);
const welcomeTab = useTab("welcome");
const infoTab = useTab("info");
diff --git a/js/apps/admin-ui/src/page-nav.css b/js/apps/admin-ui/src/page-nav.css
index 676ae04f255..bd9d20827cb 100644
--- a/js/apps/admin-ui/src/page-nav.css
+++ b/js/apps/admin-ui/src/page-nav.css
@@ -1,4 +1,5 @@
.keycloak__page_nav__nav {
- --pf-c-page__sidebar--Transition: all 50ms cubic-bezier(.42, 0, .58, 1)
+ --pf-c-page__sidebar--Transition: all 50ms cubic-bezier(.42, 0, .58, 1);
+ overflow: inherit;
}
\ No newline at end of file
diff --git a/js/libs/ui-shared/src/user-profile/utils.ts b/js/libs/ui-shared/src/user-profile/utils.ts
index b979dc7c827..329bab7fbcd 100644
--- a/js/libs/ui-shared/src/user-profile/utils.ts
+++ b/js/libs/ui-shared/src/user-profile/utils.ts
@@ -30,7 +30,7 @@ export const unWrap = (key: string) => key.substring(2, key.length - 1);
export const label = (
t: TFunction,
text: string | undefined,
- fallback: string | undefined,
+ fallback?: string,
) => (isBundleKey(text) ? t(unWrap(text!)) : text) || fallback;
export const labelAttribute = (
diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java
index 991421bd93f..a0bd21f0721 100644
--- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java
+++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java
@@ -2,7 +2,6 @@ package org.keycloak.admin.ui.rest;
import static org.keycloak.utils.StreamsUtil.throwIfEmpty;
-import java.util.Objects;
import java.util.stream.Stream;
import jakarta.ws.rs.GET;
@@ -15,6 +14,7 @@ import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.jboss.resteasy.reactive.NoCache;
+import org.keycloak.admin.ui.rest.model.RealmNameRepresentation;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.ForbiddenException;
@@ -37,26 +37,32 @@ public class UIRealmsResource {
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Operation(
- summary = "Lists only the names of the realms",
- description = "Returns a list of realm names based on what the caller is allowed to view"
+ summary = "Lists only the names and display names of the realms",
+ description = "Returns a list of realms containing only their name and displayName" +
+ " based on what the caller is allowed to view"
)
@APIResponse(
responseCode = "200",
description = "",
content = {@Content(
schema = @Schema(
- implementation = String.class,
+ implementation = RealmNameRepresentation.class,
type = SchemaType.ARRAY
)
)}
)
- public Stream getRealmNames() {
- Stream realms = session.realms().getRealmsStream()
- .filter(realm -> {
- RealmsPermissionEvaluator eval = AdminPermissions.realms(session, auth.adminAuth());
- return eval.canView(realm) || eval.isAdmin(realm);
- })
- .map(RealmModel::getName);
+ public Stream getRealms() {
+ Stream realms = session.realms().getRealmsStream()
+ .filter(realm -> {
+ RealmsPermissionEvaluator eval = AdminPermissions.realms(session, auth.adminAuth());
+ return eval.canView(realm) || eval.isAdmin(realm);
+ })
+ .map((RealmModel realm) -> {
+ RealmNameRepresentation realmNameRep = new RealmNameRepresentation();
+ realmNameRep.setDisplayName(realm.getDisplayName());
+ realmNameRep.setName(realm.getName());
+ return realmNameRep;
+ });
return throwIfEmpty(realms, new ForbiddenException());
}
}
diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RealmNameRepresentation.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RealmNameRepresentation.java
new file mode 100644
index 00000000000..06d5787a581
--- /dev/null
+++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/model/RealmNameRepresentation.java
@@ -0,0 +1,22 @@
+package org.keycloak.admin.ui.rest.model;
+
+public class RealmNameRepresentation {
+ private String name;
+ private String displayName;
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getDisplayName() {
+ return this.displayName;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+}