diff --git a/src/.gitignore b/src/.gitignore
index ae08d99..7734487 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -4,12 +4,17 @@
# account/KcAccountUi.tsx
# account/KcContext.ts
# account/KcPage.tsx
+# admin/App.tsx
# admin/index.css
# admin/KcAdminUi.tsx
+# admin/page-nav.css
+# admin/PageNav.tsx
# account/assets/logo.svg
# account/personal-info/PersonalInfo.tsx
# account/root/PageNav.tsx
# admin/assets/logo.svg
+# admin/dashboard/dashboard.css
+# admin/dashboard/Dashboard.tsx
# admin/i18n/messages_en.properties
# admin/i18n/messages_zh_Hans.properties
# admin/i18n/messages_zh_Hant.properties
@@ -18,6 +23,7 @@
# email/messages/messages_zh_CN.properties
# email/messages/messages_zh_TW.properties
# account/components/page/Page.tsx
+# admin/components/view-header/ViewHeader.tsx
# === Owned files end =====
# === @keycloakify/email-native v260007.0.0 ===
@@ -83,7 +89,6 @@
# === @keycloakify/keycloak-admin-ui v260700.0.2 ===
/admin/admin-client.tsx
-/admin/App.tsx
/admin/banners.module.css
/admin/Banners.tsx
/admin/colorScheme.ts
@@ -94,10 +99,8 @@
/admin/i18next.d.ts
/admin/KcContext.ts
/admin/KcPage.tsx
-/admin/page-nav.css
/admin/PageHeader.tsx
/admin/PageHeaderClearCachesModal.tsx
-/admin/PageNav.tsx
/admin/PageNotFoundSection.tsx
/admin/Root.tsx
/admin/routes.tsx
@@ -134,8 +137,6 @@
/admin/clients/utils.ts
/admin/components/SwitchControl.tsx
/admin/context/RecentRealms.tsx
-/admin/dashboard/dashboard.css
-/admin/dashboard/Dashboard.tsx
/admin/dashboard/ProviderInfo.tsx
/admin/dashboard/routes.ts
/admin/events/AdminEvents.tsx
@@ -522,7 +523,6 @@
/admin/components/users/UserDataTableAttributeSearchForm.tsx
/admin/components/users/UserDataTableToolbarItems.tsx
/admin/components/users/UserSelect.tsx
-/admin/components/view-header/ViewHeader.tsx
/admin/components/wizard-section-header/wizard-section-header.css
/admin/components/wizard-section-header/WizardSectionHeader.tsx
/admin/context/access/Access.tsx
diff --git a/src/admin/App.tsx b/src/admin/App.tsx
new file mode 100644
index 0000000..91f0139
--- /dev/null
+++ b/src/admin/App.tsx
@@ -0,0 +1,97 @@
+/**
+ * This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
+ * To relinquish ownership and restore this file to its original content, run the following command:
+ *
+ * $ npx keycloakify own --path "admin/App.tsx" --revert
+ */
+
+/* eslint-disable */
+
+// @ts-nocheck
+
+import { SessionExpirationWarningOverlay } from "../shared/SessionExpirationWarningOverlay";
+import {
+ ErrorBoundaryFallback,
+ ErrorBoundaryProvider,
+ KeycloakSpinner,
+ mainPageContentId
+} from "../shared/keycloak-ui-shared";
+import { Flex, FlexItem, Page } from "../shared/@patternfly/react-core";
+import { PropsWithChildren, Suspense, useEffect } from "react";
+import { Outlet } from "react-router-dom";
+import { AdminClientProvider } from "./admin-client";
+import { Header } from "./PageHeader";
+import { PageNav } from "./PageNav";
+import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
+import { ErrorRenderer } from "./components/error/ErrorRenderer";
+import { RecentRealmsProvider } from "./context/RecentRealms";
+import { AccessContextProvider } from "./context/access/Access";
+import { RealmContextProvider } from "./context/realm-context/RealmContext";
+import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
+import { WhoAmIContextProvider } from "./context/whoami/WhoAmI";
+import { SubGroups } from "./groups/SubGroupsContext";
+import { AuthWall } from "./root/AuthWall";
+import { Banners } from "./Banners";
+
+export const AppContexts = ({ children }: PropsWithChildren) => (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+);
+
+export const App = () => {
+ const hrefEndsWithHashSlash = location.href.endsWith("#/");
+ useEffect(() => {
+ if (!hrefEndsWithHashSlash) return;
+ history.replaceState(null, "", location.pathname);
+ }, [hrefEndsWithHashSlash, location.pathname]);
+
+ return (
+
+
+
+
+
+
+
+ }
+ isManagedSidebar
+ sidebar={}
+ breadcrumb={}
+ mainContainerId={mainPageContentId}
+ >
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/admin/KcAdminUi.tsx b/src/admin/KcAdminUi.tsx
index 7085064..125ea77 100644
--- a/src/admin/KcAdminUi.tsx
+++ b/src/admin/KcAdminUi.tsx
@@ -17,6 +17,7 @@ import { getKcContext } from "./KcContext";
import "./index.css";
import "../console-ui.css";
+import "./workspace.css";
const { kcContext } = getKcContext();
diff --git a/src/admin/PageNav.tsx b/src/admin/PageNav.tsx
new file mode 100644
index 0000000..fbb7635
--- /dev/null
+++ b/src/admin/PageNav.tsx
@@ -0,0 +1,233 @@
+/**
+ * This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
+ */
+
+/* eslint-disable */
+
+// @ts-nocheck
+
+import { useEnvironment } from "../shared/keycloak-ui-shared";
+import {
+ Label,
+ Nav,
+ NavGroup,
+ PageSidebar,
+ PageSidebarBody
+} from "../shared/@patternfly/react-core";
+import { FormEvent } from "react";
+import { useTranslation } from "react-i18next";
+import { NavLink, useNavigate } from "react-router-dom";
+import { useAccess } from "./context/access/Access";
+import { useRealm } from "./context/realm-context/RealmContext";
+import { useServerInfo } from "./context/server-info/ServerInfoProvider";
+import type { Environment } from "./environment-types";
+import { toPage } from "./page/routes";
+import { routes } from "./routes";
+import { resolveDisplayName } from "./util";
+import useIsFeatureEnabled, { Feature } from "./utils/useIsFeatureEnabled";
+
+import "./page-nav.css";
+
+type LeftNavProps = {
+ title: string;
+ path: string;
+ code: string;
+ id?: string;
+};
+
+const LeftNav = ({ title, path, code, id }: LeftNavProps) => {
+ const { t } = useTranslation();
+ const { hasAccess } = useAccess();
+ const { realm } = useRealm();
+ const encodedRealm = encodeURIComponent(realm);
+ const route = routes.find(
+ route => route.path.replace(/\/:.+?(\?|(?:(?!\/).)*|$)/g, "") === (id || path)
+ );
+
+ const accessAllowed =
+ route &&
+ (route.handle.access instanceof Array
+ ? hasAccess(...route.handle.access)
+ : hasAccess(route.handle.access));
+
+ if (!accessAllowed) {
+ return undefined;
+ }
+
+ const name = "nav-item" + (path || "-overview").replace("/", "-");
+ return (
+
+
+ `pf-v5-c-nav__link${isActive ? " pf-m-current" : ""}`
+ }
+ >
+
+ {code}
+
+ {t(title)}
+
+
+ );
+};
+
+export const PageNav = () => {
+ const { t } = useTranslation();
+ const { environment } = useEnvironment();
+ const { hasSomeAccess } = useAccess();
+ const { componentTypes } = useServerInfo();
+ const isFeatureEnabled = useIsFeatureEnabled();
+ const pages = componentTypes?.["org.keycloak.services.ui.extend.UiPageProvider"];
+ const navigate = useNavigate();
+ const { realm, realmRepresentation } = useRealm();
+
+ type SelectedItem = {
+ groupId: number | string;
+ itemId: number | string;
+ to: string;
+ event: FormEvent;
+ };
+
+ const onSelect = (item: SelectedItem) => {
+ navigate(item.to);
+ item.event.preventDefault();
+ };
+
+ const showManage = hasSomeAccess(
+ "view-realm",
+ "query-groups",
+ "query-users",
+ "query-clients",
+ "query-organizations",
+ "view-events"
+ );
+ const showConfigure = hasSomeAccess(
+ "view-realm",
+ "query-clients",
+ "view-identity-providers"
+ );
+ const showWorkflows =
+ hasSomeAccess("realm-admin", "admin") && isFeatureEnabled(Feature.Workflows);
+ const showManageRealm = environment.masterRealm === environment.realm;
+ const realmName = resolveDisplayName(t, realmRepresentation.displayName, realm);
+
+ return (
+
+
+
+
+ {t("adminWorkspaceRealmEyebrow")}
+
+
{realmName}
+
+
+ {realmRepresentation.enabled === false
+ ? t("disabled")
+ : t("enabled")}
+
+
+
+
+ {t("adminWorkspaceConsoleLabel")}
+
+
+
+
+ );
+};
diff --git a/src/admin/components/view-header/ViewHeader.tsx b/src/admin/components/view-header/ViewHeader.tsx
new file mode 100644
index 0000000..1b95640
--- /dev/null
+++ b/src/admin/components/view-header/ViewHeader.tsx
@@ -0,0 +1,219 @@
+/**
+ * This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
+ */
+
+/* eslint-disable */
+
+// @ts-nocheck
+
+import {
+ Badge,
+ Button,
+ Divider,
+ Dropdown,
+ DropdownList,
+ MenuToggle,
+ PageSection,
+ Switch
+} from "../../../shared/@patternfly/react-core";
+import { Fragment, ReactElement, ReactNode, isValidElement, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { HelpItem, useHelp } from "../../../shared/keycloak-ui-shared";
+import { FormattedLink } from "../external-link/FormattedLink";
+import "../../help-urls";
+
+export type ViewHeaderProps = {
+ titleKey: string;
+ className?: string;
+ badges?: ViewHeaderBadge[];
+ isDropdownDisabled?: boolean;
+ subKey?: string | ReactNode;
+ actionsDropdownId?: string;
+ helpUrl?: string | undefined;
+ dropdownItems?: ReactElement[];
+ lowerDropdownItems?: any;
+ lowerDropdownMenuTitle?: any;
+ lowerButton?: any;
+ isEnabled?: boolean;
+ onToggle?: (value: boolean) => void;
+ divider?: boolean;
+ helpTextKey?: string;
+ isReadOnly?: boolean;
+ actionDropdownTitle?: string;
+ noTranslate?: boolean;
+};
+
+export type ViewHeaderBadge = {
+ id?: string;
+ text?: string | ReactNode;
+ readonly?: boolean;
+};
+
+export const ViewHeader = ({
+ actionsDropdownId,
+ className,
+ titleKey,
+ badges,
+ isDropdownDisabled,
+ subKey,
+ helpUrl,
+ dropdownItems,
+ lowerDropdownMenuTitle,
+ lowerDropdownItems,
+ lowerButton,
+ isEnabled = true,
+ onToggle,
+ divider = true,
+ helpTextKey,
+ isReadOnly = false,
+ actionDropdownTitle = "action",
+ noTranslate = false
+}: ViewHeaderProps) => {
+ const { t, i18n } = useTranslation();
+ const { enabled } = useHelp();
+ const [isDropdownOpen, setDropdownOpen] = useState(false);
+ const [isLowerDropdownOpen, setIsLowerDropdownOpen] = useState(false);
+ const toKey = (value: string) => value.replace(/\s/g, "-");
+ const title = noTranslate || !i18n.exists(titleKey) ? titleKey : t(titleKey);
+
+ return (
+ <>
+
+
+
+
+ {t("adminWorkspaceSectionEyebrow")}
+
+
+
+ {title}
+
+ {badges?.length ? (
+
+ {badges.map((badge, index) => (
+
+ {isValidElement(badge.text) ? (
+ badge.text
+ ) : (
+
+ {badge.text}
+
+ )}
+
+ ))}
+
+ ) : null}
+
+ {enabled && (subKey || helpUrl) && (
+
+ )}
+
+ {(onToggle || dropdownItems) && (
+
+ {onToggle && (
+
+ onToggle(value)}
+ />
+ {helpTextKey && (
+
+ )}
+
+ )}
+ {dropdownItems && (
+
(
+
+ setDropdownOpen(value => !value)
+ }
+ data-testid="action-dropdown"
+ variant="primary"
+ >
+ {t(actionDropdownTitle)}
+
+ )}
+ isOpen={isDropdownOpen}
+ >
+ {dropdownItems}
+
+ )}
+
+ )}
+
+ {(lowerDropdownItems || lowerButton) && (
+
+ {lowerDropdownItems && (
+ (
+
+ setIsLowerDropdownOpen(value => !value)
+ }
+ variant="primary"
+ id="ufToggleId"
+ >
+ {t(lowerDropdownMenuTitle)}
+
+ )}
+ isOpen={isLowerDropdownOpen}
+ >
+ {lowerDropdownItems}
+
+ )}
+ {lowerButton && (
+
+ )}
+
+ )}
+
+ {divider && (
+
+ )}
+ >
+ );
+};
diff --git a/src/admin/dashboard/Dashboard.tsx b/src/admin/dashboard/Dashboard.tsx
new file mode 100644
index 0000000..d3332ec
--- /dev/null
+++ b/src/admin/dashboard/Dashboard.tsx
@@ -0,0 +1,428 @@
+/**
+ * This file has been claimed for ownership from @keycloakify/keycloak-admin-ui version 260700.0.2.
+ */
+
+/* eslint-disable */
+
+// @ts-nocheck
+
+import FeatureRepresentation, {
+ FeatureType
+} from "@keycloak/keycloak-admin-client/lib/defs/featureRepresentation";
+import {
+ HelpItem,
+ KeycloakSpinner,
+ useEnvironment
+} from "../../shared/keycloak-ui-shared";
+import {
+ Card,
+ CardBody,
+ CardTitle,
+ DescriptionList,
+ DescriptionListDescription,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ Grid,
+ GridItem,
+ Label,
+ List,
+ ListItem,
+ ListVariant,
+ PageSection,
+ Tab,
+ TabTitleText
+} from "../../shared/@patternfly/react-core";
+import { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+import { RoutableTabs, useRoutableTab } from "../components/routable-tabs/RoutableTabs";
+import { useAccess } from "../context/access/Access";
+import { useRealm } from "../context/realm-context/RealmContext";
+import { useServerInfo } from "../context/server-info/ServerInfoProvider";
+import type { Environment } from "../environment-types";
+import helpUrls from "../help-urls";
+import { resolveDisplayName } from "../util";
+import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
+import { ProviderInfo } from "./ProviderInfo";
+import { DashboardTab, toDashboard } from "./routes/Dashboard";
+
+import "./dashboard.css";
+
+type WorkspaceLink = {
+ titleKey: string;
+ descriptionKey: string;
+ path: string;
+ code: string;
+ tone: "blue" | "green" | "amber" | "coral";
+ access: string[];
+};
+
+const workspaceLinks: WorkspaceLink[] = [
+ {
+ titleKey: "users",
+ descriptionKey: "adminWorkspaceUsersDescription",
+ path: "/users",
+ code: "USR",
+ tone: "blue",
+ access: ["query-users", "view-users", "manage-users"]
+ },
+ {
+ titleKey: "clients",
+ descriptionKey: "adminWorkspaceClientsDescription",
+ path: "/clients",
+ code: "APP",
+ tone: "green",
+ access: ["query-clients", "view-clients", "manage-clients"]
+ },
+ {
+ titleKey: "groups",
+ descriptionKey: "adminWorkspaceGroupsDescription",
+ path: "/groups",
+ code: "GRP",
+ tone: "amber",
+ access: ["query-groups", "view-users", "manage-users"]
+ },
+ {
+ titleKey: "realmSettings",
+ descriptionKey: "adminWorkspaceRealmSettingsDescription",
+ path: "/realm-settings",
+ code: "CFG",
+ tone: "coral",
+ access: ["view-realm", "manage-realm"]
+ },
+ {
+ titleKey: "authentication",
+ descriptionKey: "adminWorkspaceAuthenticationDescription",
+ path: "/authentication",
+ code: "AUTH",
+ tone: "blue",
+ access: ["view-realm", "manage-realm"]
+ },
+ {
+ titleKey: "events",
+ descriptionKey: "adminWorkspaceEventsDescription",
+ path: "/events",
+ code: "EVT",
+ tone: "green",
+ access: ["view-events", "manage-events"]
+ }
+];
+
+const BooleanStatus = ({ value }: { value?: boolean }) => {
+ const { t } = useTranslation();
+ return (
+
+
+ {value ? t("enabled") : t("disabled")}
+
+ );
+};
+
+const OperationsOverview = () => {
+ const { t } = useTranslation();
+ const { realm, realmRepresentation: realmInfo } = useRealm();
+ const { hasSomeAccess } = useAccess();
+ const encodedRealm = encodeURIComponent(realm);
+ const realmDisplayName = resolveDisplayName(t, realmInfo.displayName, realm);
+ const availableLinks = workspaceLinks.filter(link => hasSomeAccess(...link.access));
+
+ const postureItems = [
+ {
+ label: t("adminWorkspaceRealmStatus"),
+ value:
+ },
+ {
+ label: t("userRegistration"),
+ value:
+ },
+ {
+ label: t("verifyEmail"),
+ value:
+ },
+ {
+ label: t("bruteForceDetection"),
+ value:
+ }
+ ];
+
+ return (
+
+
+
+
+ {t("adminWorkspaceRealmEyebrow")}
+
+
{realmDisplayName}
+
{t("adminWorkspaceOverviewDescription")}
+
+ {t("realmName")}
+ {realm}
+
+
+
+
+ IDENTITY
+
+ {availableLinks.length}
+ {t("adminWorkspaceAvailableAreas")}
+
+
+
+
+
+
+
+ {t("adminWorkspaceOperateEyebrow")}
+
{t("adminWorkspaceManageTitle")}
+
+ {t("adminWorkspaceManageDescription")}
+
+
+ {availableLinks.map(link => (
+
+
+ {link.code}
+
+
+ {t(link.titleKey)}
+ {t(link.descriptionKey)}
+
+
+ →
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+type FeatureItemProps = { feature: FeatureRepresentation };
+
+const FeatureItem = ({ feature }: FeatureItemProps) => {
+ const { t } = useTranslation();
+ const color =
+ feature.type === FeatureType.Default ||
+ feature.type === FeatureType.DisabledByDefault
+ ? "green"
+ : feature.type === FeatureType.Preview ||
+ feature.type === FeatureType.PreviewDisabledByDefault
+ ? "blue"
+ : feature.type === FeatureType.Experimental
+ ? "orange"
+ : "grey";
+ return (
+
+ {feature.name}
+
+ {feature.deprecated && feature.type !== FeatureType.Deprecated && (
+ <>
+
+ >
+ )}
+
+ );
+};
+
+const ServerInformation = () => {
+ const { t } = useTranslation();
+ const serverInfo = useServerInfo();
+ const localeSort = useLocaleSort();
+ const sortedFeatures = useMemo(
+ () => localeSort(serverInfo.features ?? [], mapByKey("name")),
+ [serverInfo.features]
+ );
+ const enabledFeatures = sortedFeatures.filter(feature => feature.enabled);
+ const disabledFeatures = sortedFeatures.filter(feature => !feature.enabled);
+
+ if (Object.keys(serverInfo).length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {t("serverInfo")}
+
+
+
+
+ {t("version")}
+
+
+ {serverInfo.systemInfo?.version}
+
+
+
+
+ {t("processorCount")}
+
+
+ {serverInfo.cpuInfo?.processorCount}
+
+
+
+
+ {t("usedMemory")}
+
+
+ {serverInfo.memoryInfo?.usedFormated} /{" "}
+ {serverInfo.memoryInfo?.totalFormated}
+
+
+
+
+
+
+
+
+ {t("profile")}
+
+
+
+
+ {t("enabledFeatures")}{" "}
+
+
+
+
+ {enabledFeatures.map(feature => (
+
+ ))}
+
+
+
+
+
+ {t("disabledFeatures")}{" "}
+
+
+
+
+ {disabledFeatures.map(feature => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const MasterDashboard = () => {
+ const { t } = useTranslation();
+ const { realm } = useRealm();
+ const useTab = (tab: DashboardTab) => useRoutableTab(toDashboard({ realm, tab }));
+ const welcomeTab = useTab("welcome");
+ const infoTab = useTab("info");
+ const providersTab = useTab("providers");
+
+ return (
+
+ {t("adminWorkspaceOverview")}}
+ {...welcomeTab}
+ >
+
+
+ {t("serverInfo")}}
+ {...infoTab}
+ >
+
+
+ {t("providerInfo")}}
+ {...providersTab}
+ >
+
+
+
+ );
+};
+
+export default function DashboardSection() {
+ const { realm } = useRealm();
+ const { environment } = useEnvironment();
+ return realm === environment.masterRealm ? (
+
+ ) : (
+
+ );
+}
diff --git a/src/admin/dashboard/dashboard.css b/src/admin/dashboard/dashboard.css
new file mode 100644
index 0000000..11e7f5b
--- /dev/null
+++ b/src/admin/dashboard/dashboard.css
@@ -0,0 +1,402 @@
+.admin-dashboard-overview {
+ padding: clamp(1.25rem, 3vw, 3rem) clamp(1.25rem, 3.2vw, 3.75rem) 5rem;
+}
+
+.admin-dashboard-overview > * {
+ width: 100%;
+ max-width: 1380px;
+ margin-inline: auto;
+}
+
+.admin-dashboard-hero {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(0, 1.45fr) minmax(220px, 0.55fr);
+ align-items: stretch;
+ overflow: hidden;
+ min-height: 260px;
+ border: 1px solid var(--console-line);
+ border-radius: 20px;
+ color: #f7fbff;
+ background: linear-gradient(135deg, #193a5b, #245a89 58%, #277e78);
+ box-shadow: 0 24px 52px rgba(23, 50, 77, 0.16);
+}
+
+.admin-dashboard-hero::before {
+ position: absolute;
+ inset: 0;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
+ background-size: 48px 48px;
+ content: "";
+ mask-image: linear-gradient(90deg, transparent, #000 65%);
+}
+
+.admin-dashboard-hero::after {
+ position: absolute;
+ inset: auto 0 0;
+ height: 5px;
+ background: linear-gradient(
+ 90deg,
+ var(--console-teal) 0 34%,
+ var(--console-amber) 34% 67%,
+ var(--console-coral) 67%
+ );
+ content: "";
+}
+
+.admin-dashboard-hero__copy {
+ position: relative;
+ z-index: 1;
+ padding: clamp(1.7rem, 4vw, 3.25rem);
+}
+
+.admin-dashboard-eyebrow,
+.admin-dashboard-section-header span,
+.admin-dashboard-posture header span,
+.admin-dashboard-guide > span {
+ display: block;
+ margin-bottom: 0.55rem;
+ color: var(--console-primary-deep);
+ font-size: 0.68rem;
+ font-weight: 800;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.admin-dashboard-eyebrow {
+ color: #92e3cf;
+}
+
+.admin-dashboard-hero h1 {
+ margin: 0;
+ color: #ffffff;
+ font-size: clamp(2.4rem, 5vw, 4.8rem);
+ font-weight: 740;
+ letter-spacing: -0.055em;
+ line-height: 0.98;
+}
+
+.admin-dashboard-hero__copy > p {
+ max-width: 60ch;
+ margin: 1.25rem 0 1.6rem;
+ color: rgba(235, 246, 255, 0.78);
+ font-size: 1rem;
+ line-height: 1.7;
+}
+
+.admin-dashboard-hero__meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.65rem 0.9rem;
+ color: rgba(235, 246, 255, 0.62);
+ font-size: 0.8rem;
+}
+
+.admin-dashboard-hero__meta strong {
+ color: #ffffff;
+ font-family: "Cascadia Code", Consolas, monospace;
+ font-weight: 600;
+}
+
+.admin-dashboard-hero .admin-dashboard-status {
+ color: rgba(235, 246, 255, 0.82);
+}
+
+.admin-dashboard-hero__signal {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ align-content: center;
+ justify-items: end;
+ padding: clamp(1.5rem, 3vw, 2.5rem);
+ border-left: 1px solid rgba(255, 255, 255, 0.13);
+ text-align: right;
+}
+
+.admin-dashboard-hero__signal > span {
+ color: rgba(235, 246, 255, 0.48);
+ font-size: 0.64rem;
+ font-weight: 800;
+ letter-spacing: 0.18em;
+}
+
+.admin-dashboard-hero__signal i {
+ width: 100%;
+ height: 1px;
+ margin: 0.75rem 0 1rem;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.38));
+}
+
+.admin-dashboard-hero__signal strong {
+ color: #ffffff;
+ font-size: clamp(3.5rem, 7vw, 6.5rem);
+ font-weight: 300;
+ letter-spacing: -0.08em;
+ line-height: 0.9;
+}
+
+.admin-dashboard-hero__signal small {
+ margin-top: 0.75rem;
+ color: rgba(235, 246, 255, 0.66);
+}
+
+.admin-dashboard-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(280px, 0.34fr);
+ align-items: start;
+ gap: clamp(1.25rem, 2.5vw, 2.25rem);
+ margin-top: clamp(1.5rem, 3vw, 2.5rem);
+}
+
+.admin-dashboard-primary,
+.admin-dashboard-posture,
+.admin-dashboard-guide {
+ border: 1px solid var(--console-line);
+ border-radius: var(--console-radius);
+ background: var(--console-surface);
+ box-shadow: var(--console-shadow);
+}
+
+.admin-dashboard-primary {
+ padding: clamp(1.25rem, 2.5vw, 2rem);
+}
+
+.admin-dashboard-section-header {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(220px, 0.55fr);
+ align-items: end;
+ gap: 1.5rem;
+ margin-bottom: 1.5rem;
+ padding-bottom: 1.25rem;
+ border-bottom: 1px solid var(--console-line);
+}
+
+.admin-dashboard-section-header h2,
+.admin-dashboard-posture h2,
+.admin-dashboard-guide h2 {
+ margin: 0;
+ color: var(--console-ink);
+ font-size: 1.35rem;
+ letter-spacing: -0.03em;
+}
+
+.admin-dashboard-section-header p,
+.admin-dashboard-guide p {
+ margin: 0;
+ color: var(--console-muted);
+ font-size: 0.88rem;
+ line-height: 1.6;
+}
+
+.admin-dashboard-link-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.8rem;
+}
+
+.admin-dashboard-link-card {
+ display: grid;
+ grid-template-columns: 3.1rem minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 0.9rem;
+ min-height: 104px;
+ padding: 1rem;
+ border: 1px solid var(--console-line);
+ border-radius: 12px;
+ color: var(--console-ink);
+ background: var(--console-surface-soft);
+ text-decoration: none;
+ transition:
+ border-color 150ms ease,
+ box-shadow 150ms ease,
+ transform 150ms ease;
+}
+
+.admin-dashboard-link-card:hover {
+ border-color: color-mix(in srgb, var(--card-tone) 45%, var(--console-line));
+ color: var(--console-ink);
+ box-shadow: 0 12px 28px rgba(23, 50, 77, 0.09);
+ text-decoration: none;
+ transform: translateY(-2px);
+}
+
+.admin-dashboard-link-card.is-blue {
+ --card-tone: var(--console-primary);
+}
+.admin-dashboard-link-card.is-green {
+ --card-tone: var(--console-teal);
+}
+.admin-dashboard-link-card.is-amber {
+ --card-tone: var(--console-amber);
+}
+.admin-dashboard-link-card.is-coral {
+ --card-tone: var(--console-coral);
+}
+
+.admin-dashboard-link-card__code {
+ display: grid;
+ width: 3.1rem;
+ height: 3.1rem;
+ border-radius: 11px;
+ color: color-mix(in srgb, var(--card-tone) 86%, var(--console-ink));
+ background: color-mix(in srgb, var(--card-tone) 13%, var(--console-surface));
+ font-family: "Cascadia Code", Consolas, monospace;
+ font-size: 0.62rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ place-items: center;
+}
+
+.admin-dashboard-link-card__copy {
+ display: grid;
+ gap: 0.25rem;
+}
+
+.admin-dashboard-link-card__copy strong {
+ font-size: 0.98rem;
+}
+
+.admin-dashboard-link-card__copy small {
+ color: var(--console-muted);
+ line-height: 1.45;
+}
+
+.admin-dashboard-link-card__arrow {
+ color: var(--card-tone);
+ font-size: 1.2rem;
+}
+
+.admin-dashboard-aside {
+ display: grid;
+ gap: 1rem;
+}
+
+.admin-dashboard-posture,
+.admin-dashboard-guide {
+ padding: 1.35rem;
+}
+
+.admin-dashboard-posture dl {
+ margin: 1.1rem 0 0;
+}
+
+.admin-dashboard-posture dl > div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.85rem 0;
+ border-top: 1px solid var(--console-line);
+}
+
+.admin-dashboard-posture dt {
+ color: var(--console-muted);
+ font-size: 0.82rem;
+}
+
+.admin-dashboard-posture dd {
+ margin: 0;
+}
+
+.admin-dashboard-status {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.42rem;
+ color: var(--console-muted);
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.admin-dashboard-status i {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--console-coral);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-coral) 12%, transparent);
+}
+
+.admin-dashboard-status.is-positive i {
+ background: var(--console-teal);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-teal) 12%, transparent);
+}
+
+.admin-dashboard-text-link,
+.admin-dashboard-guide a {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--console-line);
+ color: var(--console-primary-deep);
+ font-size: 0.82rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.admin-dashboard-guide {
+ background: linear-gradient(
+ 145deg,
+ color-mix(in srgb, var(--console-primary) 8%, var(--console-surface)),
+ var(--console-surface)
+ );
+}
+
+.admin-dashboard-guide p {
+ margin-top: 0.7rem;
+}
+
+.admin-dashboard-tabs > .pf-v5-c-tabs {
+ padding-inline: clamp(1.25rem, 3.2vw, 3.75rem);
+ background: var(--console-surface);
+}
+
+.admin-dashboard-technical {
+ padding: clamp(1.25rem, 3vw, 3rem) clamp(1.25rem, 3.2vw, 3.75rem) 5rem;
+}
+
+.keycloak__dashboard_card {
+ height: 100%;
+}
+
+@media (max-width: 1050px) {
+ .admin-dashboard-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-dashboard-aside {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .admin-dashboard-overview {
+ padding: 1rem 1rem 3.5rem;
+ }
+
+ .admin-dashboard-hero {
+ grid-template-columns: 1fr;
+ min-height: 0;
+ border-radius: 14px;
+ }
+
+ .admin-dashboard-hero__signal {
+ display: none;
+ }
+
+ .admin-dashboard-hero h1 {
+ font-size: clamp(2.25rem, 12vw, 3.5rem);
+ }
+
+ .admin-dashboard-section-header,
+ .admin-dashboard-link-grid,
+ .admin-dashboard-aside {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-dashboard-link-card {
+ min-height: 92px;
+ }
+}
diff --git a/src/admin/i18n/messages_en_override.properties b/src/admin/i18n/messages_en_override.properties
new file mode 100644
index 0000000..bfdc19c
--- /dev/null
+++ b/src/admin/i18n/messages_en_override.properties
@@ -0,0 +1,22 @@
+adminWorkspaceOverview=Realm overview
+adminWorkspaceRealmEyebrow=Realm workspace
+adminWorkspaceConsoleLabel=Identity operations
+adminWorkspaceSectionEyebrow=Administration workspace
+adminWorkspaceOverviewDescription=Manage identities, applications, access and security posture from one operational workspace.
+adminWorkspaceAvailableAreas=available areas
+adminWorkspaceOperateEyebrow=Operations
+adminWorkspaceManageTitle=Manage this realm
+adminWorkspaceManageDescription=Open the area that matches the task you need to complete. Only areas available to your role are shown.
+adminWorkspaceUsersDescription=Find people, maintain profiles and manage credentials.
+adminWorkspaceClientsDescription=Configure applications, protocols and client access.
+adminWorkspaceGroupsDescription=Organize people and apply shared access rules.
+adminWorkspaceRealmSettingsDescription=Control realm behavior, themes, tokens and policies.
+adminWorkspaceAuthenticationDescription=Design sign-in flows and required actions.
+adminWorkspaceEventsDescription=Review user and administrator activity.
+adminWorkspacePostureEyebrow=Current state
+adminWorkspacePostureTitle=Security posture
+adminWorkspaceRealmStatus=Realm status
+adminWorkspaceReviewSettings=Review realm settings
+adminWorkspaceHelpEyebrow=Reference
+adminWorkspaceHelpTitle=Need implementation details?
+adminWorkspaceHelpDescription=Use the official documentation when changing protocols, authentication flows or production security settings.
diff --git a/src/admin/i18n/messages_zh_Hans_override.properties b/src/admin/i18n/messages_zh_Hans_override.properties
new file mode 100644
index 0000000..a6a9104
--- /dev/null
+++ b/src/admin/i18n/messages_zh_Hans_override.properties
@@ -0,0 +1,22 @@
+adminWorkspaceOverview=领域总览
+adminWorkspaceRealmEyebrow=领域工作区
+adminWorkspaceConsoleLabel=身份运营
+adminWorkspaceSectionEyebrow=管理工作区
+adminWorkspaceOverviewDescription=在一个工作区中管理身份、应用、访问权限与安全配置。
+adminWorkspaceAvailableAreas=个可用业务区
+adminWorkspaceOperateEyebrow=业务操作
+adminWorkspaceManageTitle=管理当前领域
+adminWorkspaceManageDescription=从当前任务出发进入对应业务区,仅显示你有权限操作的功能。
+adminWorkspaceUsersDescription=查找人员、维护资料并管理登录凭据。
+adminWorkspaceClientsDescription=配置应用、协议以及客户端访问规则。
+adminWorkspaceGroupsDescription=组织人员并应用统一的访问权限。
+adminWorkspaceRealmSettingsDescription=管理领域行为、主题、令牌和安全策略。
+adminWorkspaceAuthenticationDescription=设计登录流程和必要操作。
+adminWorkspaceEventsDescription=查看用户及管理员的操作记录。
+adminWorkspacePostureEyebrow=当前状态
+adminWorkspacePostureTitle=安全配置概览
+adminWorkspaceRealmStatus=领域状态
+adminWorkspaceReviewSettings=检查领域设置
+adminWorkspaceHelpEyebrow=使用参考
+adminWorkspaceHelpTitle=需要配置说明?
+adminWorkspaceHelpDescription=修改协议、认证流程或生产安全设置时,可查阅官方文档。
diff --git a/src/admin/i18n/messages_zh_Hant_override.properties b/src/admin/i18n/messages_zh_Hant_override.properties
new file mode 100644
index 0000000..1008c00
--- /dev/null
+++ b/src/admin/i18n/messages_zh_Hant_override.properties
@@ -0,0 +1,22 @@
+adminWorkspaceOverview=領域總覽
+adminWorkspaceRealmEyebrow=領域工作區
+adminWorkspaceConsoleLabel=身分營運
+adminWorkspaceSectionEyebrow=管理工作區
+adminWorkspaceOverviewDescription=在同一個工作區中管理身分、應用程式、存取權限與安全設定。
+adminWorkspaceAvailableAreas=個可用業務區
+adminWorkspaceOperateEyebrow=業務操作
+adminWorkspaceManageTitle=管理目前領域
+adminWorkspaceManageDescription=從目前任務進入對應業務區,僅顯示你有權限操作的功能。
+adminWorkspaceUsersDescription=尋找人員、維護資料並管理登入憑證。
+adminWorkspaceClientsDescription=設定應用程式、協定與用戶端存取規則。
+adminWorkspaceGroupsDescription=組織人員並套用統一的存取權限。
+adminWorkspaceRealmSettingsDescription=管理領域行為、主題、權杖與安全政策。
+adminWorkspaceAuthenticationDescription=設計登入流程與必要操作。
+adminWorkspaceEventsDescription=查看使用者及管理員的操作記錄。
+adminWorkspacePostureEyebrow=目前狀態
+adminWorkspacePostureTitle=安全設定概覽
+adminWorkspaceRealmStatus=領域狀態
+adminWorkspaceReviewSettings=檢查領域設定
+adminWorkspaceHelpEyebrow=使用參考
+adminWorkspaceHelpTitle=需要設定說明?
+adminWorkspaceHelpDescription=修改協定、驗證流程或正式環境安全設定時,可查閱官方文件。
diff --git a/src/admin/page-nav.css b/src/admin/page-nav.css
new file mode 100644
index 0000000..139a834
--- /dev/null
+++ b/src/admin/page-nav.css
@@ -0,0 +1,96 @@
+.keycloak__page_nav__nav {
+ --pf-v5-c-page__sidebar--Transition: all 120ms cubic-bezier(0.2, 0.7, 0.2, 1);
+ overflow: inherit;
+ overflow-y: auto;
+}
+
+.admin-nav-context {
+ display: grid;
+ gap: 0.32rem;
+ margin: 0 0.35rem 1rem;
+ padding: 0.2rem 0.45rem 1.15rem;
+ border-bottom: 1px solid var(--console-line);
+}
+
+.admin-nav-context__eyebrow {
+ color: var(--console-primary-deep);
+ font-size: 0.64rem;
+ font-weight: 800;
+ letter-spacing: 0.11em;
+ text-transform: uppercase;
+}
+
+.admin-nav-context strong {
+ overflow: hidden;
+ color: var(--console-ink);
+ font-size: 1.08rem;
+ font-weight: 720;
+ letter-spacing: -0.015em;
+ text-overflow: ellipsis;
+}
+
+.admin-nav-context__meta {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ color: var(--console-muted);
+ font-size: 0.72rem;
+ font-weight: 650;
+}
+
+.admin-nav-status {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--console-teal);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-teal) 12%, transparent);
+}
+
+.admin-nav-status.is-disabled {
+ background: var(--console-coral);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-coral) 12%, transparent);
+}
+
+.keycloak__page_nav__nav .pf-v5-c-nav__link {
+ display: grid;
+ grid-template-columns: 2.25rem minmax(0, 1fr);
+ align-items: center;
+ gap: 0.68rem;
+}
+
+.admin-nav-code {
+ display: grid;
+ width: 2.25rem;
+ height: 1.75rem;
+ border: 1px solid color-mix(in srgb, var(--console-line-strong) 62%, transparent);
+ border-radius: 7px;
+ color: var(--console-faint);
+ background: color-mix(in srgb, var(--console-surface) 58%, transparent);
+ font-family: "Cascadia Code", Consolas, monospace;
+ font-size: 0.53rem;
+ font-weight: 750;
+ letter-spacing: 0.025em;
+ place-items: center;
+}
+
+.pf-v5-c-nav__link:hover .admin-nav-code,
+.pf-v5-c-nav__link:focus .admin-nav-code,
+.pf-v5-c-nav__link.pf-m-current .admin-nav-code {
+ border-color: color-mix(in srgb, var(--console-primary) 30%, var(--console-line));
+ color: var(--console-primary-deep);
+ background: color-mix(in srgb, var(--console-primary) 8%, var(--console-surface));
+}
+
+.admin-nav-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ margin: 1.4rem 0.4rem 0;
+ padding: 1rem 0.4rem 0;
+ border-top: 1px solid var(--console-line);
+ color: var(--console-faint);
+ font-size: 0.65rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+}
diff --git a/src/admin/workspace.css b/src/admin/workspace.css
new file mode 100644
index 0000000..658489d
--- /dev/null
+++ b/src/admin/workspace.css
@@ -0,0 +1,152 @@
+.admin-workspace-route {
+ min-width: 0;
+ min-height: 100%;
+}
+
+.admin-view-header {
+ position: relative;
+ padding-top: clamp(1.7rem, 3vw, 2.7rem);
+ padding-bottom: clamp(1.25rem, 2.5vw, 2rem);
+ background: transparent;
+}
+
+.admin-view-header::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: clamp(1.25rem, 3.2vw, 3.75rem);
+ width: 3px;
+ border-radius: 99px;
+ background: linear-gradient(
+ 180deg,
+ var(--console-teal) 0 34%,
+ var(--console-amber) 34% 67%,
+ var(--console-coral) 67%
+ );
+ content: "";
+}
+
+.admin-view-header__layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: end;
+ gap: 1.5rem;
+ padding-left: 1.25rem;
+}
+
+.admin-view-header__copy {
+ min-width: 0;
+}
+
+.admin-view-header__eyebrow {
+ display: block;
+ margin-bottom: 0.55rem;
+ color: var(--console-primary-deep);
+ font-size: 0.67rem;
+ font-weight: 800;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.admin-view-header__title-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.65rem 0.9rem;
+}
+
+.admin-view-header h1 {
+ margin: 0;
+ color: var(--console-ink);
+ font-size: clamp(2rem, 3.2vw, 3rem);
+ font-weight: 730;
+ letter-spacing: -0.045em;
+ line-height: 1.04;
+}
+
+.admin-view-header__badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+}
+
+.admin-view-header__description {
+ max-width: 76ch;
+ margin: 0.8rem 0 0;
+ color: var(--console-muted);
+ line-height: 1.65;
+}
+
+.admin-view-header__actions,
+.admin-view-header__switch,
+.admin-view-header__lower-actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.8rem;
+}
+
+.admin-view-header__actions {
+ justify-content: flex-end;
+}
+
+.admin-view-header__lower-actions {
+ margin-top: 1.25rem;
+ padding-left: 1.25rem;
+}
+
+.admin-view-header__divider {
+ border-color: var(--console-line);
+}
+
+/* List routes become a clear content deck below the shared business header. */
+.admin-workspace-route > .pf-v5-c-page__main-section:not(.admin-view-header),
+.admin-workspace-route > .pf-v5-c-tabs,
+.admin-workspace-route > form,
+.admin-workspace-route > .keycloak__form {
+ position: relative;
+}
+
+.admin-workspace-route .pf-v5-c-toolbar + .pf-v5-c-table,
+.admin-workspace-route .pf-v5-c-toolbar + .pf-v5-c-data-list {
+ margin-top: 0.75rem;
+}
+
+.admin-workspace-route .pf-v5-c-toolbar {
+ padding: 0.35rem 0;
+}
+
+.admin-workspace-route .pf-v5-c-table {
+ --pf-v5-c-table--cell--PaddingTop: 0.9rem;
+ --pf-v5-c-table--cell--PaddingBottom: 0.9rem;
+}
+
+.admin-workspace-route .pf-v5-c-form__section-title {
+ margin-top: 1.7rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--console-line);
+ color: var(--console-ink);
+ font-size: 1.12rem;
+ font-weight: 700;
+}
+
+@media (max-width: 768px) {
+ .admin-view-header::before {
+ left: 1rem;
+ }
+
+ .admin-view-header__layout {
+ grid-template-columns: 1fr;
+ align-items: start;
+ gap: 1rem;
+ padding-left: 1rem;
+ }
+
+ .admin-view-header__actions {
+ justify-content: flex-start;
+ }
+
+ .admin-view-header h1 {
+ font-size: 2rem;
+ }
+}