From 676c73c82bea71cdba0e6afab13027340f82810e Mon Sep 17 00:00:00 2001 From: biss Date: Tue, 4 Aug 2026 16:48:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E6=94=B9=E4=B8=BA=E2=80=9C?= =?UTF-8?q?=E9=A2=86=E5=9F=9F=E8=BF=90=E8=90=A5=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E2=80=9D=EF=BC=8C=E9=9D=9E=20master=20=E9=A2=86=E5=9F=9F?= =?UTF-8?q?=E4=B9=9F=E6=9C=89=E5=AE=8C=E6=95=B4=E7=AE=A1=E7=90=86=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E3=80=82=20=E6=A0=B9=E6=8D=AE=E6=9D=83=E9=99=90?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E7=94=A8=E6=88=B7=E3=80=81=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E3=80=81=E7=BB=84=E3=80=81=E9=A2=86=E5=9F=9F=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E3=80=81=E8=AE=A4=E8=AF=81=E3=80=81=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E7=AD=89=E4=B8=9A=E5=8A=A1=E5=85=A5=E5=8F=A3=E3=80=82=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=89=E5=85=A8=E9=85=8D=E7=BD=AE=E6=A6=82?= =?UTF-8?q?=E8=A7=88=EF=BC=9A=E9=A2=86=E5=9F=9F=E7=8A=B6=E6=80=81=E3=80=81?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E3=80=81=E9=82=AE=E7=AE=B1=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E3=80=81=E6=9A=B4=E5=8A=9B=E7=A0=B4=E8=A7=A3=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=E3=80=82=20=E4=BE=A7=E6=A0=8F=E5=A2=9E=E5=8A=A0=E9=A2=86?= =?UTF-8?q?=E5=9F=9F=E4=B8=8A=E4=B8=8B=E6=96=87=E3=80=81=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=92=8C=E4=B8=9A=E5=8A=A1=E5=88=86=E7=BB=84=E3=80=82=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=80=9A=E7=94=A8=E9=A1=B5=E9=9D=A2=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=BB=9F=E4=B8=80=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E3=80=81=E8=AF=B4=E6=98=8E=E3=80=81=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E3=80=81=E5=BC=80=E5=85=B3=E5=92=8C=E6=93=8D=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E3=80=82=20=E5=A2=9E=E5=8A=A0=E7=BB=9F=E4=B8=80=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E5=AE=B9=E5=99=A8=EF=BC=8C=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A1=A8=E6=A0=BC=E3=80=81=E8=A1=A8=E5=8D=95=E5=8F=8A?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E5=B8=83=E5=B1=80=E3=80=82=20?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=AE=80=E4=BD=93=E4=B8=AD=E6=96=87=E3=80=81?= =?UTF-8?q?=E7=B9=81=E4=BD=93=E4=B8=AD=E6=96=87=E5=92=8C=E8=8B=B1=E6=96=87?= =?UTF-8?q?=E6=96=87=E6=A1=88=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.gitignore | 12 +- src/admin/App.tsx | 97 ++++ src/admin/KcAdminUi.tsx | 1 + src/admin/PageNav.tsx | 233 ++++++++++ .../components/view-header/ViewHeader.tsx | 219 +++++++++ src/admin/dashboard/Dashboard.tsx | 428 ++++++++++++++++++ src/admin/dashboard/dashboard.css | 402 ++++++++++++++++ .../i18n/messages_en_override.properties | 22 + .../i18n/messages_zh_Hans_override.properties | 22 + .../i18n/messages_zh_Hant_override.properties | 22 + src/admin/page-nav.css | 96 ++++ src/admin/workspace.css | 152 +++++++ 12 files changed, 1700 insertions(+), 6 deletions(-) create mode 100644 src/admin/App.tsx create mode 100644 src/admin/PageNav.tsx create mode 100644 src/admin/components/view-header/ViewHeader.tsx create mode 100644 src/admin/dashboard/Dashboard.tsx create mode 100644 src/admin/dashboard/dashboard.css create mode 100644 src/admin/i18n/messages_en_override.properties create mode 100644 src/admin/i18n/messages_zh_Hans_override.properties create mode 100644 src/admin/i18n/messages_zh_Hant_override.properties create mode 100644 src/admin/page-nav.css create mode 100644 src/admin/workspace.css 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" : ""}` + } + > + + {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} +
    +
    +
    + +
    + {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) && ( +

    + {isValidElement(subKey) + ? subKey + : subKey + ? t(subKey as string) + : ""} + {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 ( + +