首页改为“领域运营工作台”,非 master 领域也有完整管理首页。
根据权限展示用户、客户端、组、领域设置、认证、事件等业务入口。 新增安全配置概览:领域状态、注册、邮箱验证、暴力破解保护。 侧栏增加领域上下文、状态和业务分组。 重构通用页面标题组件,统一标题、说明、状态、开关和操作区。 增加统一工作区容器,并优化表格、表单及详情页布局。 补充简体中文、繁体中文和英文文案。
This commit is contained in:
+6
-6
@@ -4,12 +4,17 @@
|
|||||||
# account/KcAccountUi.tsx
|
# account/KcAccountUi.tsx
|
||||||
# account/KcContext.ts
|
# account/KcContext.ts
|
||||||
# account/KcPage.tsx
|
# account/KcPage.tsx
|
||||||
|
# admin/App.tsx
|
||||||
# admin/index.css
|
# admin/index.css
|
||||||
# admin/KcAdminUi.tsx
|
# admin/KcAdminUi.tsx
|
||||||
|
# admin/page-nav.css
|
||||||
|
# admin/PageNav.tsx
|
||||||
# account/assets/logo.svg
|
# account/assets/logo.svg
|
||||||
# account/personal-info/PersonalInfo.tsx
|
# account/personal-info/PersonalInfo.tsx
|
||||||
# account/root/PageNav.tsx
|
# account/root/PageNav.tsx
|
||||||
# admin/assets/logo.svg
|
# admin/assets/logo.svg
|
||||||
|
# admin/dashboard/dashboard.css
|
||||||
|
# admin/dashboard/Dashboard.tsx
|
||||||
# admin/i18n/messages_en.properties
|
# admin/i18n/messages_en.properties
|
||||||
# admin/i18n/messages_zh_Hans.properties
|
# admin/i18n/messages_zh_Hans.properties
|
||||||
# admin/i18n/messages_zh_Hant.properties
|
# admin/i18n/messages_zh_Hant.properties
|
||||||
@@ -18,6 +23,7 @@
|
|||||||
# email/messages/messages_zh_CN.properties
|
# email/messages/messages_zh_CN.properties
|
||||||
# email/messages/messages_zh_TW.properties
|
# email/messages/messages_zh_TW.properties
|
||||||
# account/components/page/Page.tsx
|
# account/components/page/Page.tsx
|
||||||
|
# admin/components/view-header/ViewHeader.tsx
|
||||||
# === Owned files end =====
|
# === Owned files end =====
|
||||||
|
|
||||||
# === @keycloakify/email-native v260007.0.0 ===
|
# === @keycloakify/email-native v260007.0.0 ===
|
||||||
@@ -83,7 +89,6 @@
|
|||||||
|
|
||||||
# === @keycloakify/keycloak-admin-ui v260700.0.2 ===
|
# === @keycloakify/keycloak-admin-ui v260700.0.2 ===
|
||||||
/admin/admin-client.tsx
|
/admin/admin-client.tsx
|
||||||
/admin/App.tsx
|
|
||||||
/admin/banners.module.css
|
/admin/banners.module.css
|
||||||
/admin/Banners.tsx
|
/admin/Banners.tsx
|
||||||
/admin/colorScheme.ts
|
/admin/colorScheme.ts
|
||||||
@@ -94,10 +99,8 @@
|
|||||||
/admin/i18next.d.ts
|
/admin/i18next.d.ts
|
||||||
/admin/KcContext.ts
|
/admin/KcContext.ts
|
||||||
/admin/KcPage.tsx
|
/admin/KcPage.tsx
|
||||||
/admin/page-nav.css
|
|
||||||
/admin/PageHeader.tsx
|
/admin/PageHeader.tsx
|
||||||
/admin/PageHeaderClearCachesModal.tsx
|
/admin/PageHeaderClearCachesModal.tsx
|
||||||
/admin/PageNav.tsx
|
|
||||||
/admin/PageNotFoundSection.tsx
|
/admin/PageNotFoundSection.tsx
|
||||||
/admin/Root.tsx
|
/admin/Root.tsx
|
||||||
/admin/routes.tsx
|
/admin/routes.tsx
|
||||||
@@ -134,8 +137,6 @@
|
|||||||
/admin/clients/utils.ts
|
/admin/clients/utils.ts
|
||||||
/admin/components/SwitchControl.tsx
|
/admin/components/SwitchControl.tsx
|
||||||
/admin/context/RecentRealms.tsx
|
/admin/context/RecentRealms.tsx
|
||||||
/admin/dashboard/dashboard.css
|
|
||||||
/admin/dashboard/Dashboard.tsx
|
|
||||||
/admin/dashboard/ProviderInfo.tsx
|
/admin/dashboard/ProviderInfo.tsx
|
||||||
/admin/dashboard/routes.ts
|
/admin/dashboard/routes.ts
|
||||||
/admin/events/AdminEvents.tsx
|
/admin/events/AdminEvents.tsx
|
||||||
@@ -522,7 +523,6 @@
|
|||||||
/admin/components/users/UserDataTableAttributeSearchForm.tsx
|
/admin/components/users/UserDataTableAttributeSearchForm.tsx
|
||||||
/admin/components/users/UserDataTableToolbarItems.tsx
|
/admin/components/users/UserDataTableToolbarItems.tsx
|
||||||
/admin/components/users/UserSelect.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/wizard-section-header.css
|
||||||
/admin/components/wizard-section-header/WizardSectionHeader.tsx
|
/admin/components/wizard-section-header/WizardSectionHeader.tsx
|
||||||
/admin/context/access/Access.tsx
|
/admin/context/access/Access.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) => (
|
||||||
|
<ErrorBoundaryProvider>
|
||||||
|
<ErrorBoundaryFallback fallback={ErrorRenderer}>
|
||||||
|
<ServerInfoProvider>
|
||||||
|
<RealmContextProvider>
|
||||||
|
<WhoAmIContextProvider>
|
||||||
|
<RecentRealmsProvider>
|
||||||
|
<AccessContextProvider>
|
||||||
|
<SubGroups>{children}</SubGroups>
|
||||||
|
</AccessContextProvider>
|
||||||
|
</RecentRealmsProvider>
|
||||||
|
</WhoAmIContextProvider>
|
||||||
|
</RealmContextProvider>
|
||||||
|
</ServerInfoProvider>
|
||||||
|
</ErrorBoundaryFallback>
|
||||||
|
</ErrorBoundaryProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
const hrefEndsWithHashSlash = location.href.endsWith("#/");
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hrefEndsWithHashSlash) return;
|
||||||
|
history.replaceState(null, "", location.pathname);
|
||||||
|
}, [hrefEndsWithHashSlash, location.pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminClientProvider>
|
||||||
|
<AppContexts>
|
||||||
|
<Flex
|
||||||
|
direction={{ default: "column" }}
|
||||||
|
flexWrap={{ default: "nowrap" }}
|
||||||
|
spaceItems={{ default: "spaceItemsNone" }}
|
||||||
|
style={{ height: "100%" }}
|
||||||
|
>
|
||||||
|
<FlexItem>
|
||||||
|
<Banners />
|
||||||
|
</FlexItem>
|
||||||
|
<FlexItem grow={{ default: "grow" }} style={{ minHeight: 0 }}>
|
||||||
|
<Page
|
||||||
|
header={<Header />}
|
||||||
|
isManagedSidebar
|
||||||
|
sidebar={<PageNav />}
|
||||||
|
breadcrumb={<PageBreadCrumbs />}
|
||||||
|
mainContainerId={mainPageContentId}
|
||||||
|
>
|
||||||
|
<ErrorBoundaryFallback fallback={ErrorRenderer}>
|
||||||
|
<Suspense fallback={<KeycloakSpinner />}>
|
||||||
|
<AuthWall>
|
||||||
|
<div className="admin-workspace-route">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</AuthWall>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundaryFallback>
|
||||||
|
</Page>
|
||||||
|
</FlexItem>
|
||||||
|
</Flex>
|
||||||
|
<SessionExpirationWarningOverlay warnUserSecondsBeforeAutoLogout={45} />
|
||||||
|
</AppContexts>
|
||||||
|
</AdminClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@ import { getKcContext } from "./KcContext";
|
|||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import "../console-ui.css";
|
import "../console-ui.css";
|
||||||
|
import "./workspace.css";
|
||||||
|
|
||||||
const { kcContext } = getKcContext();
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
id={name}
|
||||||
|
data-testid={name}
|
||||||
|
to={`/${encodedRealm}${path}`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`pf-v5-c-nav__link${isActive ? " pf-m-current" : ""}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="admin-nav-code" aria-hidden="true">
|
||||||
|
{code}
|
||||||
|
</span>
|
||||||
|
<span>{t(title)}</span>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageNav = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { environment } = useEnvironment<Environment>();
|
||||||
|
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<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<PageSidebar className="keycloak__page_nav__nav">
|
||||||
|
<PageSidebarBody>
|
||||||
|
<div className="admin-nav-context">
|
||||||
|
<span className="admin-nav-context__eyebrow">
|
||||||
|
{t("adminWorkspaceRealmEyebrow")}
|
||||||
|
</span>
|
||||||
|
<strong data-testid="currentRealm">{realmName}</strong>
|
||||||
|
<div className="admin-nav-context__meta">
|
||||||
|
<span
|
||||||
|
className={`admin-nav-status${
|
||||||
|
realmRepresentation.enabled === false
|
||||||
|
? " is-disabled"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{realmRepresentation.enabled === false
|
||||||
|
? t("disabled")
|
||||||
|
: t("enabled")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Nav onSelect={(_event, item) => onSelect(item as SelectedItem)}>
|
||||||
|
<NavGroup>
|
||||||
|
<LeftNav
|
||||||
|
title="adminWorkspaceOverview"
|
||||||
|
path=""
|
||||||
|
id="/"
|
||||||
|
code="OVR"
|
||||||
|
/>
|
||||||
|
</NavGroup>
|
||||||
|
{showManageRealm && (
|
||||||
|
<NavGroup>
|
||||||
|
<LeftNav title="manageRealms" path="/realms" code="RLM" />
|
||||||
|
</NavGroup>
|
||||||
|
)}
|
||||||
|
{showManage && (
|
||||||
|
<NavGroup aria-label={t("manage")} title={t("manage")}>
|
||||||
|
{isFeatureEnabled(Feature.Organizations) &&
|
||||||
|
realmRepresentation.organizationsEnabled && (
|
||||||
|
<LeftNav
|
||||||
|
title="organizations"
|
||||||
|
path="/organizations"
|
||||||
|
code="ORG"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LeftNav title="clients" path="/clients" code="APP" />
|
||||||
|
<LeftNav
|
||||||
|
title="clientScopes"
|
||||||
|
path="/client-scopes"
|
||||||
|
code="SCP"
|
||||||
|
/>
|
||||||
|
<LeftNav title="realmRoles" path="/roles" code="ROL" />
|
||||||
|
<LeftNav title="users" path="/users" code="USR" />
|
||||||
|
<LeftNav title="groups" path="/groups" code="GRP" />
|
||||||
|
<LeftNav title="sessions" path="/sessions" code="SES" />
|
||||||
|
<LeftNav title="events" path="/events" code="EVT" />
|
||||||
|
</NavGroup>
|
||||||
|
)}
|
||||||
|
{showConfigure && (
|
||||||
|
<NavGroup aria-label={t("configure")} title={t("configure")}>
|
||||||
|
<LeftNav
|
||||||
|
title="realmSettings"
|
||||||
|
path="/realm-settings"
|
||||||
|
code="CFG"
|
||||||
|
/>
|
||||||
|
<LeftNav
|
||||||
|
title="authentication"
|
||||||
|
path="/authentication"
|
||||||
|
code="AUTH"
|
||||||
|
/>
|
||||||
|
{isFeatureEnabled(Feature.AdminFineGrainedAuthzV2) &&
|
||||||
|
realmRepresentation.adminPermissionsEnabled && (
|
||||||
|
<LeftNav
|
||||||
|
title="permissions"
|
||||||
|
path="/permissions"
|
||||||
|
code="POL"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LeftNav
|
||||||
|
title="identityProviders"
|
||||||
|
path="/identity-providers"
|
||||||
|
code="IDP"
|
||||||
|
/>
|
||||||
|
<LeftNav
|
||||||
|
title="userFederation"
|
||||||
|
path="/user-federation"
|
||||||
|
code="DIR"
|
||||||
|
/>
|
||||||
|
{showWorkflows && (
|
||||||
|
<LeftNav
|
||||||
|
title="workflows"
|
||||||
|
path="/workflows"
|
||||||
|
code="FLOW"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isFeatureEnabled(Feature.DeclarativeUI) &&
|
||||||
|
pages?.map(page => (
|
||||||
|
<LeftNav
|
||||||
|
key={page.id}
|
||||||
|
title={page.id}
|
||||||
|
path={toPage({ providerId: page.id }).pathname!}
|
||||||
|
id="/page-section"
|
||||||
|
code="EXT"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NavGroup>
|
||||||
|
)}
|
||||||
|
</Nav>
|
||||||
|
<div className="admin-nav-footer">
|
||||||
|
<span>{t("adminWorkspaceConsoleLabel")}</span>
|
||||||
|
<Label color="blue">{t("currentRealm")}</Label>
|
||||||
|
</div>
|
||||||
|
</PageSidebarBody>
|
||||||
|
</PageSidebar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<PageSection variant="light" className="admin-view-header">
|
||||||
|
<div className="admin-view-header__layout">
|
||||||
|
<div className="admin-view-header__copy">
|
||||||
|
<span className="admin-view-header__eyebrow">
|
||||||
|
{t("adminWorkspaceSectionEyebrow")}
|
||||||
|
</span>
|
||||||
|
<div className="admin-view-header__title-row">
|
||||||
|
<h1 className={className} data-testid="view-header">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{badges?.length ? (
|
||||||
|
<div className="admin-view-header__badges">
|
||||||
|
{badges.map((badge, index) => (
|
||||||
|
<Fragment key={badge.id ?? index}>
|
||||||
|
{isValidElement(badge.text) ? (
|
||||||
|
badge.text
|
||||||
|
) : (
|
||||||
|
<Badge
|
||||||
|
data-testid={badge.id}
|
||||||
|
isRead={badge.readonly}
|
||||||
|
>
|
||||||
|
{badge.text}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{enabled && (subKey || helpUrl) && (
|
||||||
|
<p
|
||||||
|
id="view-header-subkey"
|
||||||
|
className="admin-view-header__description"
|
||||||
|
>
|
||||||
|
{isValidElement(subKey)
|
||||||
|
? subKey
|
||||||
|
: subKey
|
||||||
|
? t(subKey as string)
|
||||||
|
: ""}
|
||||||
|
{helpUrl && (
|
||||||
|
<FormattedLink
|
||||||
|
title={t("learnMore")}
|
||||||
|
href={helpUrl}
|
||||||
|
isInline
|
||||||
|
className="pf-v5-u-ml-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(onToggle || dropdownItems) && (
|
||||||
|
<div className="admin-view-header__actions">
|
||||||
|
{onToggle && (
|
||||||
|
<div className="admin-view-header__switch">
|
||||||
|
<Switch
|
||||||
|
id={`${toKey(titleKey)}-switch`}
|
||||||
|
data-testid={`${titleKey}-switch`}
|
||||||
|
label={t("enabled")}
|
||||||
|
labelOff={t("disabled")}
|
||||||
|
isDisabled={isReadOnly}
|
||||||
|
isChecked={isEnabled}
|
||||||
|
aria-label={t("enabled")}
|
||||||
|
onChange={(_event, value) => onToggle(value)}
|
||||||
|
/>
|
||||||
|
{helpTextKey && (
|
||||||
|
<HelpItem
|
||||||
|
helpText={t(helpTextKey)}
|
||||||
|
fieldLabelId={`${toKey(titleKey)}-switch`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{dropdownItems && (
|
||||||
|
<Dropdown
|
||||||
|
popperProps={{ position: "right" }}
|
||||||
|
onOpenChange={setDropdownOpen}
|
||||||
|
toggle={ref => (
|
||||||
|
<MenuToggle
|
||||||
|
ref={ref}
|
||||||
|
isDisabled={isDropdownDisabled}
|
||||||
|
id={actionsDropdownId}
|
||||||
|
onClick={() =>
|
||||||
|
setDropdownOpen(value => !value)
|
||||||
|
}
|
||||||
|
data-testid="action-dropdown"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
{t(actionDropdownTitle)}
|
||||||
|
</MenuToggle>
|
||||||
|
)}
|
||||||
|
isOpen={isDropdownOpen}
|
||||||
|
>
|
||||||
|
<DropdownList>{dropdownItems}</DropdownList>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{(lowerDropdownItems || lowerButton) && (
|
||||||
|
<div className="admin-view-header__lower-actions">
|
||||||
|
{lowerDropdownItems && (
|
||||||
|
<Dropdown
|
||||||
|
className="keycloak__user-federation__dropdown"
|
||||||
|
onOpenChange={setIsLowerDropdownOpen}
|
||||||
|
toggle={ref => (
|
||||||
|
<MenuToggle
|
||||||
|
ref={ref}
|
||||||
|
onClick={() =>
|
||||||
|
setIsLowerDropdownOpen(value => !value)
|
||||||
|
}
|
||||||
|
variant="primary"
|
||||||
|
id="ufToggleId"
|
||||||
|
>
|
||||||
|
{t(lowerDropdownMenuTitle)}
|
||||||
|
</MenuToggle>
|
||||||
|
)}
|
||||||
|
isOpen={isLowerDropdownOpen}
|
||||||
|
>
|
||||||
|
<DropdownList>{lowerDropdownItems}</DropdownList>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
{lowerButton && (
|
||||||
|
<Button
|
||||||
|
variant={lowerButton.variant}
|
||||||
|
onClick={lowerButton.onClick}
|
||||||
|
data-testid="viewHeader-lower-btn"
|
||||||
|
>
|
||||||
|
{lowerButton.lowerButtonTitle}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
{divider && (
|
||||||
|
<Divider component="div" className="admin-view-header__divider" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<span className={`admin-dashboard-status${value ? " is-positive" : ""}`}>
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
{value ? t("enabled") : t("disabled")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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: <BooleanStatus value={realmInfo.enabled !== false} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("userRegistration"),
|
||||||
|
value: <BooleanStatus value={realmInfo.registrationAllowed === true} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("verifyEmail"),
|
||||||
|
value: <BooleanStatus value={realmInfo.verifyEmail === true} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("bruteForceDetection"),
|
||||||
|
value: <BooleanStatus value={realmInfo.bruteForceProtected === true} />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-dashboard-overview">
|
||||||
|
<section className="admin-dashboard-hero">
|
||||||
|
<div className="admin-dashboard-hero__copy">
|
||||||
|
<span className="admin-dashboard-eyebrow">
|
||||||
|
{t("adminWorkspaceRealmEyebrow")}
|
||||||
|
</span>
|
||||||
|
<h1>{realmDisplayName}</h1>
|
||||||
|
<p>{t("adminWorkspaceOverviewDescription")}</p>
|
||||||
|
<div className="admin-dashboard-hero__meta">
|
||||||
|
<span>{t("realmName")}</span>
|
||||||
|
<strong>{realm}</strong>
|
||||||
|
<BooleanStatus value={realmInfo.enabled !== false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="admin-dashboard-hero__signal" aria-hidden="true">
|
||||||
|
<span>IDENTITY</span>
|
||||||
|
<i />
|
||||||
|
<strong>{availableLinks.length}</strong>
|
||||||
|
<small>{t("adminWorkspaceAvailableAreas")}</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="admin-dashboard-layout">
|
||||||
|
<section
|
||||||
|
className="admin-dashboard-primary"
|
||||||
|
aria-labelledby="workspace-title"
|
||||||
|
>
|
||||||
|
<header className="admin-dashboard-section-header">
|
||||||
|
<div>
|
||||||
|
<span>{t("adminWorkspaceOperateEyebrow")}</span>
|
||||||
|
<h2 id="workspace-title">{t("adminWorkspaceManageTitle")}</h2>
|
||||||
|
</div>
|
||||||
|
<p>{t("adminWorkspaceManageDescription")}</p>
|
||||||
|
</header>
|
||||||
|
<div className="admin-dashboard-link-grid">
|
||||||
|
{availableLinks.map(link => (
|
||||||
|
<Link
|
||||||
|
key={link.path}
|
||||||
|
to={`/${encodedRealm}${link.path}`}
|
||||||
|
className={`admin-dashboard-link-card is-${link.tone}`}
|
||||||
|
>
|
||||||
|
<span className="admin-dashboard-link-card__code">
|
||||||
|
{link.code}
|
||||||
|
</span>
|
||||||
|
<span className="admin-dashboard-link-card__copy">
|
||||||
|
<strong>{t(link.titleKey)}</strong>
|
||||||
|
<small>{t(link.descriptionKey)}</small>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="admin-dashboard-link-card__arrow"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside className="admin-dashboard-aside">
|
||||||
|
<section className="admin-dashboard-posture">
|
||||||
|
<header>
|
||||||
|
<span>{t("adminWorkspacePostureEyebrow")}</span>
|
||||||
|
<h2>{t("adminWorkspacePostureTitle")}</h2>
|
||||||
|
</header>
|
||||||
|
<dl>
|
||||||
|
{postureItems.map(item => (
|
||||||
|
<div key={item.label}>
|
||||||
|
<dt>{item.label}</dt>
|
||||||
|
<dd>{item.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
{hasSomeAccess("view-realm", "manage-realm") && (
|
||||||
|
<Link
|
||||||
|
className="admin-dashboard-text-link"
|
||||||
|
to={`/${encodedRealm}/realm-settings`}
|
||||||
|
>
|
||||||
|
{t("adminWorkspaceReviewSettings")} <span>→</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<section className="admin-dashboard-guide">
|
||||||
|
<span>{t("adminWorkspaceHelpEyebrow")}</span>
|
||||||
|
<h2>{t("adminWorkspaceHelpTitle")}</h2>
|
||||||
|
<p>{t("adminWorkspaceHelpDescription")}</p>
|
||||||
|
<a href={helpUrls.documentation} target="_blank" rel="noreferrer">
|
||||||
|
{t("viewDocumentation")} <span>↗</span>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ListItem className="pf-v5-u-mb-sm">
|
||||||
|
{feature.name}
|
||||||
|
<Label color={color}>{t(feature.type.toLowerCase())}</Label>
|
||||||
|
{feature.deprecated && feature.type !== FeatureType.Deprecated && (
|
||||||
|
<>
|
||||||
|
<Label color="grey">{t("deprecated")}</Label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <KeycloakSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection variant="light" className="admin-dashboard-technical">
|
||||||
|
<Grid hasGutter>
|
||||||
|
<GridItem lg={4} sm={12}>
|
||||||
|
<Card className="keycloak__dashboard_card">
|
||||||
|
<CardTitle>{t("serverInfo")}</CardTitle>
|
||||||
|
<CardBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListGroup>
|
||||||
|
<DescriptionListTerm>
|
||||||
|
{t("version")}
|
||||||
|
</DescriptionListTerm>
|
||||||
|
<DescriptionListDescription>
|
||||||
|
{serverInfo.systemInfo?.version}
|
||||||
|
</DescriptionListDescription>
|
||||||
|
</DescriptionListGroup>
|
||||||
|
<DescriptionListGroup>
|
||||||
|
<DescriptionListTerm>
|
||||||
|
{t("processorCount")}
|
||||||
|
</DescriptionListTerm>
|
||||||
|
<DescriptionListDescription>
|
||||||
|
{serverInfo.cpuInfo?.processorCount}
|
||||||
|
</DescriptionListDescription>
|
||||||
|
</DescriptionListGroup>
|
||||||
|
<DescriptionListGroup>
|
||||||
|
<DescriptionListTerm>
|
||||||
|
{t("usedMemory")}
|
||||||
|
</DescriptionListTerm>
|
||||||
|
<DescriptionListDescription>
|
||||||
|
{serverInfo.memoryInfo?.usedFormated} /{" "}
|
||||||
|
{serverInfo.memoryInfo?.totalFormated}
|
||||||
|
</DescriptionListDescription>
|
||||||
|
</DescriptionListGroup>
|
||||||
|
</DescriptionList>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</GridItem>
|
||||||
|
<GridItem lg={8} sm={12}>
|
||||||
|
<Card className="keycloak__dashboard_card">
|
||||||
|
<CardTitle>{t("profile")}</CardTitle>
|
||||||
|
<CardBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListGroup>
|
||||||
|
<DescriptionListTerm>
|
||||||
|
{t("enabledFeatures")}{" "}
|
||||||
|
<HelpItem
|
||||||
|
fieldLabelId="enabledFeatures"
|
||||||
|
helpText={t("infoEnabledFeatures")}
|
||||||
|
/>
|
||||||
|
</DescriptionListTerm>
|
||||||
|
<DescriptionListDescription>
|
||||||
|
<List variant={ListVariant.inline}>
|
||||||
|
{enabledFeatures.map(feature => (
|
||||||
|
<FeatureItem
|
||||||
|
key={feature.name}
|
||||||
|
feature={feature}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</DescriptionListDescription>
|
||||||
|
</DescriptionListGroup>
|
||||||
|
<DescriptionListGroup>
|
||||||
|
<DescriptionListTerm>
|
||||||
|
{t("disabledFeatures")}{" "}
|
||||||
|
<HelpItem
|
||||||
|
fieldLabelId="disabledFeatures"
|
||||||
|
helpText={t("infoDisabledFeatures")}
|
||||||
|
/>
|
||||||
|
</DescriptionListTerm>
|
||||||
|
<DescriptionListDescription>
|
||||||
|
<List variant={ListVariant.inline}>
|
||||||
|
{disabledFeatures.map(feature => (
|
||||||
|
<FeatureItem
|
||||||
|
key={feature.name}
|
||||||
|
feature={feature}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</DescriptionListDescription>
|
||||||
|
</DescriptionListGroup>
|
||||||
|
</DescriptionList>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<RoutableTabs
|
||||||
|
data-testid="dashboard-tabs"
|
||||||
|
defaultLocation={toDashboard({ realm, tab: "welcome" })}
|
||||||
|
isBox
|
||||||
|
mountOnEnter
|
||||||
|
className="admin-dashboard-tabs"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
id="welcome"
|
||||||
|
data-testid="welcomeTab"
|
||||||
|
title={<TabTitleText>{t("adminWorkspaceOverview")}</TabTitleText>}
|
||||||
|
{...welcomeTab}
|
||||||
|
>
|
||||||
|
<OperationsOverview />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
id="info"
|
||||||
|
data-testid="infoTab"
|
||||||
|
title={<TabTitleText>{t("serverInfo")}</TabTitleText>}
|
||||||
|
{...infoTab}
|
||||||
|
>
|
||||||
|
<ServerInformation />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
id="providers"
|
||||||
|
data-testid="providersTab"
|
||||||
|
title={<TabTitleText>{t("providerInfo")}</TabTitleText>}
|
||||||
|
{...providersTab}
|
||||||
|
>
|
||||||
|
<ProviderInfo />
|
||||||
|
</Tab>
|
||||||
|
</RoutableTabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DashboardSection() {
|
||||||
|
const { realm } = useRealm();
|
||||||
|
const { environment } = useEnvironment<Environment>();
|
||||||
|
return realm === environment.masterRealm ? (
|
||||||
|
<MasterDashboard />
|
||||||
|
) : (
|
||||||
|
<OperationsOverview />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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=修改协议、认证流程或生产安全设置时,可查阅官方文档。
|
||||||
@@ -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=修改協定、驗證流程或正式環境安全設定時,可查閱官方文件。
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user