Compare commits
6
Commits
78743fb436
..
main
+14
-14
@@ -1,13 +1,25 @@
|
||||
# This file is managed by Keycloakify, do not edit it manually.
|
||||
|
||||
# === Owned files start ===
|
||||
# account/colorScheme.ts
|
||||
# account/KcAccountUi.tsx
|
||||
# account/KcContext.ts
|
||||
# account/KcPage.tsx
|
||||
# admin/App.tsx
|
||||
# admin/colorScheme.ts
|
||||
# admin/index.css
|
||||
# admin/KcAdminUi.tsx
|
||||
# admin/page-nav.css
|
||||
# admin/PageHeader.tsx
|
||||
# admin/PageNav.tsx
|
||||
# account/assets/logo.svg
|
||||
# account/personal-info/PersonalInfo.tsx
|
||||
# account/root/Header.tsx
|
||||
# account/root/PageNav.tsx
|
||||
# account/root/Root.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
|
||||
@@ -15,6 +27,8 @@
|
||||
# email/messages/messages_en.properties
|
||||
# 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 ===
|
||||
@@ -80,10 +94,8 @@
|
||||
|
||||
# === @keycloakify/keycloak-admin-ui v260700.0.2 ===
|
||||
/admin/admin-client.tsx
|
||||
/admin/App.tsx
|
||||
/admin/banners.module.css
|
||||
/admin/Banners.tsx
|
||||
/admin/colorScheme.ts
|
||||
/admin/environment-types.ts
|
||||
/admin/environment.ts
|
||||
/admin/ForbiddenSection.tsx
|
||||
@@ -91,10 +103,7 @@
|
||||
/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
|
||||
@@ -131,8 +140,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
|
||||
@@ -519,7 +526,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
|
||||
@@ -838,7 +844,6 @@
|
||||
|
||||
# === @keycloakify/keycloak-account-ui v260700.0.3 ===
|
||||
/account/api.ts
|
||||
/account/colorScheme.ts
|
||||
/account/environment.ts
|
||||
/account/i18n-type.ts
|
||||
/account/i18next.d.ts
|
||||
@@ -904,7 +909,6 @@
|
||||
/account/i18n/messages_zh_Hans.properties
|
||||
/account/i18n/messages_zh_Hant.properties
|
||||
/account/organizations/Organizations.tsx
|
||||
/account/personal-info/PersonalInfo.tsx
|
||||
/account/resources/EditTheResource.tsx
|
||||
/account/resources/PermissionRequest.tsx
|
||||
/account/resources/Resources.tsx
|
||||
@@ -913,9 +917,6 @@
|
||||
/account/resources/SharedWith.tsx
|
||||
/account/resources/ShareTheResource.tsx
|
||||
/account/root/header.module.css
|
||||
/account/root/Header.tsx
|
||||
/account/root/PageNav.tsx
|
||||
/account/root/Root.tsx
|
||||
/account/utils/formatDate.ts
|
||||
/account/utils/joinPath.ts
|
||||
/account/utils/useAccountAlerts.ts
|
||||
@@ -1074,4 +1075,3 @@
|
||||
/account/assets/passkeys/zoho-light.svg
|
||||
/account/assets/passkeys/ztpass-light.png
|
||||
/account/components/datalist/EmptyRow.tsx
|
||||
/account/components/page/Page.tsx
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import "@patternfly/patternfly/patternfly-addons.css";
|
||||
import "@patternfly/react-core/dist/styles/base.css";
|
||||
import "./theme.css";
|
||||
import "../console-ui.css";
|
||||
import "./workspace.css";
|
||||
|
||||
import { useReducer, useEffect } from "react";
|
||||
import { startColorSchemeManagement } from "./colorScheme";
|
||||
@@ -16,8 +18,12 @@ import { environment } from "./environment";
|
||||
import { i18n } from "./i18n/i18n";
|
||||
import { Root } from "./root/Root";
|
||||
import { SessionExpirationWarningOverlay } from "../shared/SessionExpirationWarningOverlay";
|
||||
import { getKcContext } from "./KcContext";
|
||||
|
||||
document.title = "NEXUS / ID";
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
document.documentElement.dataset.kcThemeName = kcContext.themeName;
|
||||
document.title = kcContext.themeName === "class" ? "612 / 账户中心" : "NEXUS / ID";
|
||||
|
||||
const prI18nInitialized = i18n.init();
|
||||
startColorSchemeManagement();
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title">
|
||||
<title id="title">NEXUS ID</title>
|
||||
<defs>
|
||||
<linearGradient id="signal" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#63e6ff"/>
|
||||
<stop offset="1" stop-color="#8b7cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(8 9)">
|
||||
<rect x="5" y="5" width="24" height="24" rx="1" fill="none" stroke="url(#signal)" stroke-width="1.5" transform="rotate(45 17 17)"/>
|
||||
<circle cx="17" cy="17" r="3.5" fill="#63e6ff"/>
|
||||
<circle cx="17" cy="17" r="8.5" fill="none" stroke="#63e6ff" stroke-opacity=".18"/>
|
||||
<rect width="34" height="34" rx="10" fill="#356fd6"/>
|
||||
<path d="M9 10h16M9 17h11M9 24h16" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="24.5" cy="17" r="2.2" fill="#f0b92e"/>
|
||||
</g>
|
||||
<text x="52" y="32" fill="#f4f7ff" font-family="Bahnschrift,Segoe UI,sans-serif" font-size="20" font-weight="600" letter-spacing="3">NEXUS</text>
|
||||
<text x="151" y="32" fill="#95a2be" font-family="Bahnschrift,Segoe UI,sans-serif" font-size="18" font-weight="400" letter-spacing="2">/ ID</text>
|
||||
<text x="54" y="31" fill="#17324d" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="19" font-weight="700" letter-spacing="1.2">NEXUS</text>
|
||||
<text x="133" y="31" fill="#667a8e" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="15" font-weight="600" letter-spacing="1">WORKSPACE</text>
|
||||
<rect x="54" y="38" width="31" height="3" rx="1.5" fill="#279477"/>
|
||||
<rect x="88" y="38" width="31" height="3" rx="1.5" fill="#e9ad38"/>
|
||||
<rect x="122" y="38" width="31" height="3" rx="1.5" fill="#e86452"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 929 B |
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/colorScheme.ts" --revert
|
||||
*/
|
||||
|
||||
/* IMPORTANT NOTE:
|
||||
* If you modify how the light/dark mode is implemented
|
||||
* you must also update public/admin/early-color-scheme.js
|
||||
* This scrip is ran before this to avoid white flashes
|
||||
*/
|
||||
|
||||
import { getKcContext } from "./KcContext";
|
||||
import { startConsoleColorScheme } from "../shared/consoleColorScheme";
|
||||
|
||||
export function startColorSchemeManagement() {
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
startConsoleColorScheme(kcContext.darkMode !== false);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/components/page/Page.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
PageSection,
|
||||
Text,
|
||||
TextContent,
|
||||
Title
|
||||
} from "../../../shared/@patternfly/react-core";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
type PageProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const Page = ({ title, description, children }: PropsWithChildren<PageProps>) => {
|
||||
return (
|
||||
<article className="account-business-page">
|
||||
<PageSection variant="light" className="account-view-header">
|
||||
<div className="account-view-header__layout">
|
||||
<div className="account-view-header__copy">
|
||||
<span className="account-view-header__eyebrow">
|
||||
Identity workspace / Account
|
||||
</span>
|
||||
<TextContent>
|
||||
<Title headingLevel="h1" data-testid="page-heading">
|
||||
{title}
|
||||
</Title>
|
||||
<Text component="p">{description}</Text>
|
||||
</TextContent>
|
||||
</div>
|
||||
<div className="account-view-header__context" aria-hidden="true">
|
||||
<span>SELF SERVICE</span>
|
||||
<strong>ID</strong>
|
||||
<i />
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
<PageSection variant="light" className="account-view-body">
|
||||
<div className="account-view-body__surface">{children}</div>
|
||||
</PageSection>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/personal-info/PersonalInfo.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
UserProfileFields,
|
||||
beerify,
|
||||
debeerify,
|
||||
setUserProfileServerError,
|
||||
useEnvironment
|
||||
} from "../../shared/keycloak-ui-shared";
|
||||
import {
|
||||
ActionGroup,
|
||||
Alert,
|
||||
AlertVariant,
|
||||
Button,
|
||||
ExpandableSection,
|
||||
Form,
|
||||
Spinner
|
||||
} from "../../shared/@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
|
||||
import { TFunction } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { ErrorOption, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getPersonalInfo, getSupportedLocales, savePersonalInfo } from "../api/methods";
|
||||
import { UserProfileMetadata, UserRepresentation } from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { type AccountEnvironment } from "..";
|
||||
import type { TFuncKey } from "../i18n-type";
|
||||
import { useAccountAlerts } from "../utils/useAccountAlerts";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
export const PersonalInfo = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
const [userProfileMetadata, setUserProfileMetadata] = useState<UserProfileMetadata>();
|
||||
const [supportedLocales, setSupportedLocales] = useState<string[]>([]);
|
||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||
const { handleSubmit, reset, setValue, setError } = form;
|
||||
const profile = form.watch();
|
||||
const { addAlert } = useAccountAlerts();
|
||||
|
||||
usePromise(
|
||||
signal =>
|
||||
Promise.all([
|
||||
getPersonalInfo({ signal, context }),
|
||||
getSupportedLocales({ signal, context })
|
||||
]),
|
||||
([personalInfo, supportedLocales]) => {
|
||||
setUserProfileMetadata(personalInfo.userProfileMetadata);
|
||||
setSupportedLocales(supportedLocales);
|
||||
reset(personalInfo);
|
||||
Object.entries(personalInfo.attributes || {}).forEach(([k, v]) =>
|
||||
setValue(`attributes[${beerify(k)}]`, v)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async (user: UserRepresentation) => {
|
||||
try {
|
||||
const attributes = Object.fromEntries(
|
||||
Object.entries(user.attributes || {}).map(([k, v]) => [debeerify(k), v])
|
||||
);
|
||||
await savePersonalInfo(context, { ...user, attributes });
|
||||
const locale = attributes["locale"]?.toString();
|
||||
if (locale) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("languageChanged", { detail: { language: locale } })
|
||||
);
|
||||
}
|
||||
await context.keycloak.updateToken();
|
||||
addAlert(t("accountUpdatedMessage"));
|
||||
} catch (error) {
|
||||
addAlert(t("accountUpdatedError"), AlertVariant.danger);
|
||||
|
||||
setUserProfileServerError(
|
||||
{ responseData: { errors: error as any } },
|
||||
(name: string | number, error: unknown) =>
|
||||
setError(name as string, error as ErrorOption),
|
||||
((key: TFuncKey, param?: object) => t(key, param as any)) as TFunction
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!userProfileMetadata) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const allFieldsReadOnly = () =>
|
||||
userProfileMetadata.attributes
|
||||
.map(a => a.readOnly)
|
||||
.reduce((p, c) => p && c, true);
|
||||
|
||||
const {
|
||||
updateEmailFeatureEnabled,
|
||||
updateEmailActionEnabled,
|
||||
isRegistrationEmailAsUsername,
|
||||
isEditUserNameAllowed
|
||||
} = context.environment.features;
|
||||
|
||||
const displayName =
|
||||
[profile.firstName, profile.lastName].filter(Boolean).join(" ").trim() ||
|
||||
profile.username ||
|
||||
t("personalInfo");
|
||||
const initials =
|
||||
[profile.firstName, profile.lastName]
|
||||
.filter(Boolean)
|
||||
.map(value => value!.trim().charAt(0))
|
||||
.join("")
|
||||
.slice(0, 2)
|
||||
.toUpperCase() || displayName.slice(0, 2).toUpperCase();
|
||||
const profileLocale =
|
||||
profile.attributes?.locale?.toString() || context.environment.locale;
|
||||
|
||||
return (
|
||||
<Page title={t("personalInfo")} description={t("personalInfoDescription")}>
|
||||
<div className="nexus-profile-layout">
|
||||
<aside className="nexus-identity-card" aria-label={t("personalInfo")}>
|
||||
<div className="nexus-identity-card__topline">
|
||||
<span>Identity record</span>
|
||||
<i aria-hidden="true" />
|
||||
</div>
|
||||
<div className="nexus-identity-card__avatar" aria-hidden="true">
|
||||
<span>{initials}</span>
|
||||
</div>
|
||||
<div className="nexus-identity-card__name">
|
||||
<span>{t("personalInfo")}</span>
|
||||
<h2>{displayName}</h2>
|
||||
<p>{profile.email || profile.username}</p>
|
||||
</div>
|
||||
<dl className="nexus-identity-card__facts">
|
||||
<div>
|
||||
<dt>{t("username")}</dt>
|
||||
<dd>{profile.username || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{t("email")}</dt>
|
||||
<dd>{profile.email || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Locale</dt>
|
||||
<dd>{profileLocale}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="nexus-identity-card__footer">
|
||||
<span aria-hidden="true" />
|
||||
NEXUS identity directory
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="nexus-profile-editor">
|
||||
<header className="nexus-profile-editor__header">
|
||||
<span>Profile data</span>
|
||||
<div>
|
||||
<h2>{t("personalInfo")}</h2>
|
||||
<p>{t("personalInfoDescription")}</p>
|
||||
</div>
|
||||
</header>
|
||||
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
|
||||
<UserProfileFields
|
||||
form={form}
|
||||
userProfileMetadata={userProfileMetadata}
|
||||
supportedLocales={supportedLocales}
|
||||
currentLocale={context.environment.locale}
|
||||
t={
|
||||
((key: unknown, params) =>
|
||||
t(key as TFuncKey, params as any)) as TFunction
|
||||
}
|
||||
renderer={attribute => {
|
||||
const annotations = attribute.annotations
|
||||
? attribute.annotations
|
||||
: {};
|
||||
return attribute.name === "email" &&
|
||||
updateEmailFeatureEnabled &&
|
||||
updateEmailActionEnabled &&
|
||||
annotations["kc.required.action.supported"] &&
|
||||
(!isRegistrationEmailAsUsername ||
|
||||
isEditUserNameAllowed) ? (
|
||||
<Button
|
||||
id="update-email-btn"
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
context.keycloak.login({
|
||||
action: "UPDATE_EMAIL"
|
||||
})
|
||||
}
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
>
|
||||
{t("updateEmail")}
|
||||
</Button>
|
||||
) : undefined;
|
||||
}}
|
||||
/>
|
||||
{!allFieldsReadOnly() && (
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="save"
|
||||
type="submit"
|
||||
id="save-btn"
|
||||
variant="primary"
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
id="cancel-btn"
|
||||
variant="link"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
)}
|
||||
{context.environment.features.deleteAccountAllowed && (
|
||||
<ExpandableSection
|
||||
data-testid="delete-account"
|
||||
toggleText={t("deleteAccount")}
|
||||
>
|
||||
<Alert
|
||||
isInline
|
||||
title={t("deleteAccount")}
|
||||
variant="danger"
|
||||
actionLinks={
|
||||
<Button
|
||||
id="delete-account-btn"
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
context.keycloak.login({
|
||||
action: "delete_account"
|
||||
})
|
||||
}
|
||||
className="delete-button"
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{t("deleteAccountWarning")}
|
||||
</Alert>
|
||||
</ExpandableSection>
|
||||
)}
|
||||
</Form>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalInfo;
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/Header.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import logoSvgUrl from "../assets/logo.svg";
|
||||
import { KeycloakMasthead, label, useEnvironment } from "../../shared/keycloak-ui-shared";
|
||||
import { Button } from "../../shared/@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "../../shared/@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHref } from "react-router-dom";
|
||||
|
||||
import { AccountEnvironment } from "..";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { ThemeModeToggle } from "../../shared/ThemeModeToggle";
|
||||
|
||||
import style from "./header.module.css";
|
||||
|
||||
const ReferrerLink = () => {
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return environment.referrerUrl ? (
|
||||
<Button
|
||||
data-testid="referrer-link"
|
||||
component="a"
|
||||
href={environment.referrerUrl.replace("_hash_", "#")}
|
||||
variant="link"
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
>
|
||||
{t("backTo", {
|
||||
app: label(t, environment.referrerName, environment.referrerUrl)
|
||||
})}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : "/";
|
||||
const internalLogoHref = useHref(logoUrl);
|
||||
|
||||
// User can indicate that he wants an internal URL by starting it with "/"
|
||||
const indexHref = logoUrl.startsWith("/") ? internalLogoHref : logoUrl;
|
||||
|
||||
return (
|
||||
<KeycloakMasthead
|
||||
data-testid="page-header"
|
||||
keycloak={keycloak}
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: indexHref,
|
||||
src: logoSvgUrl,
|
||||
alt: t("logo"),
|
||||
className: style.brand
|
||||
}}
|
||||
toolbarItems={[
|
||||
<ThemeModeToggle key="theme-mode" />,
|
||||
<ReferrerLink key="link" />
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/PageNav.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEnvironment } from "../../shared/keycloak-ui-shared";
|
||||
import {
|
||||
Nav,
|
||||
NavExpandable,
|
||||
NavItem,
|
||||
NavList,
|
||||
PageSidebar,
|
||||
PageSidebarBody,
|
||||
Spinner
|
||||
} from "../../shared/@patternfly/react-core";
|
||||
import {
|
||||
Children,
|
||||
PropsWithChildren,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
Suspense,
|
||||
useMemo,
|
||||
useState
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { matchPath, useHref, useLinkClickHandler, useLocation } from "react-router-dom";
|
||||
import {
|
||||
ApplicationsIcon,
|
||||
BuildingIcon,
|
||||
DesktopIcon,
|
||||
FolderOpenAltIcon,
|
||||
KeyIcon,
|
||||
LinkIcon,
|
||||
ShieldAltIcon,
|
||||
UserAltIcon,
|
||||
UserFriendsIcon
|
||||
} from "../../shared/@patternfly/react-icons";
|
||||
|
||||
import fetchContentJson from "../content/fetchContent";
|
||||
import type { TFuncKey } from "../i18n-type";
|
||||
import type { AccountEnvironment, Feature } from "..";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
type RootMenuItem = {
|
||||
id?: string;
|
||||
label: TFuncKey;
|
||||
path: string;
|
||||
isVisible?: keyof Feature;
|
||||
modulePath?: string;
|
||||
};
|
||||
|
||||
type MenuItemWithChildren = {
|
||||
label: TFuncKey;
|
||||
children: MenuItem[];
|
||||
isVisible?: keyof Feature;
|
||||
};
|
||||
|
||||
export type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||
|
||||
export const PageNav = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>();
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
|
||||
usePromise(signal => fetchContentJson({ signal, context }), setMenuItems);
|
||||
return (
|
||||
<PageSidebar>
|
||||
<PageSidebarBody>
|
||||
<div className="nexus-nav-context">
|
||||
<span className="nexus-nav-context__eyebrow">NEXUS / ID</span>
|
||||
<strong>Identity control</strong>
|
||||
<span className="nexus-nav-context__rule" aria-hidden="true" />
|
||||
</div>
|
||||
<Nav>
|
||||
<NavList>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{menuItems
|
||||
?.filter(menuItem =>
|
||||
menuItem.isVisible
|
||||
? context.environment.features[menuItem.isVisible]
|
||||
: true
|
||||
)
|
||||
.map(menuItem => (
|
||||
<NavMenuItem
|
||||
key={menuItem.label as string}
|
||||
menuItem={menuItem}
|
||||
/>
|
||||
))}
|
||||
</Suspense>
|
||||
</NavList>
|
||||
</Nav>
|
||||
</PageSidebarBody>
|
||||
</PageSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
const menuIcons: Record<string, JSX.Element> = {
|
||||
personalInfo: <UserAltIcon />,
|
||||
accountSecurity: <ShieldAltIcon />,
|
||||
signingIn: <KeyIcon />,
|
||||
deviceActivity: <DesktopIcon />,
|
||||
linkedAccounts: <LinkIcon />,
|
||||
applications: <ApplicationsIcon />,
|
||||
verifiableCredentials: <ShieldAltIcon />,
|
||||
groups: <UserFriendsIcon />,
|
||||
organizations: <BuildingIcon />,
|
||||
resources: <FolderOpenAltIcon />
|
||||
};
|
||||
|
||||
const NavLabel = ({ label }: { label: TFuncKey }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<span className="nexus-nav-label">
|
||||
<span className="nexus-nav-label__icon" aria-hidden="true">
|
||||
{menuIcons[label as string] ?? <UserAltIcon />}
|
||||
</span>
|
||||
<span>{t(label)}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type NavMenuItemProps = {
|
||||
menuItem: MenuItem;
|
||||
};
|
||||
|
||||
function NavMenuItem({ menuItem }: NavMenuItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const { pathname } = useLocation();
|
||||
const isActive = useMemo(
|
||||
() => matchMenuItem(pathname, menuItem, environment.baseUrl),
|
||||
[pathname, menuItem, environment.baseUrl]
|
||||
);
|
||||
|
||||
if ("path" in menuItem) {
|
||||
return (
|
||||
<NavLink path={menuItem.path} isActive={isActive}>
|
||||
<NavLabel label={menuItem.label} />
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavExpandable
|
||||
data-testid={menuItem.label}
|
||||
title={<NavLabel label={menuItem.label} />}
|
||||
isActive={isActive}
|
||||
isExpanded={isActive}
|
||||
>
|
||||
{menuItem.children
|
||||
.filter(menuItem =>
|
||||
menuItem.isVisible ? environment.features[menuItem.isVisible] : true
|
||||
)
|
||||
.map(child => (
|
||||
<NavMenuItem key={child.label as string} menuItem={child} />
|
||||
))}
|
||||
</NavExpandable>
|
||||
);
|
||||
}
|
||||
|
||||
function getFullUrl(path: string, baseUrl: string) {
|
||||
return `${new URL(baseUrl).pathname}${path}`;
|
||||
}
|
||||
|
||||
function matchMenuItem(
|
||||
currentPath: string,
|
||||
menuItem: MenuItem,
|
||||
baseUrl: string
|
||||
): boolean {
|
||||
if ("path" in menuItem) {
|
||||
return !!matchPath(getFullUrl(menuItem.path, baseUrl), currentPath);
|
||||
}
|
||||
|
||||
return menuItem.children.some(child => matchMenuItem(currentPath, child, baseUrl));
|
||||
}
|
||||
|
||||
type NavLinkProps = {
|
||||
path: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export const NavLink = ({
|
||||
path,
|
||||
isActive,
|
||||
children
|
||||
}: PropsWithChildren<NavLinkProps>) => {
|
||||
const { environment } = useEnvironment<AccountEnvironment>();
|
||||
const menuItemPath = getFullUrl(path, environment.baseUrl) + location.search;
|
||||
const href = useHref(menuItemPath);
|
||||
const handleClick = useLinkClickHandler(menuItemPath);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
data-testid={path}
|
||||
to={href}
|
||||
isActive={isActive}
|
||||
onClick={event =>
|
||||
// PatternFly does not have the correct type for this event, so we need to cast it.
|
||||
handleClick(event as unknown as ReactMouseEvent<HTMLAnchorElement>)
|
||||
}
|
||||
>
|
||||
{Children.toArray(children)}
|
||||
</NavItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* This file has been claimed for ownership from @keycloakify/keycloak-account-ui version 260700.0.3.
|
||||
* To relinquish ownership and restore this file to its original content, run the following command:
|
||||
*
|
||||
* $ npx keycloakify own --path "account/root/Root.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
ErrorPage,
|
||||
useAlerts,
|
||||
useEnvironment,
|
||||
KeycloakContext
|
||||
} from "../../shared/keycloak-ui-shared";
|
||||
import { AlertVariant, Page, Spinner } from "../../shared/@patternfly/react-core";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
Outlet,
|
||||
RouteObject,
|
||||
RouterProvider
|
||||
} from "react-router-dom";
|
||||
import fetchContentJson from "../content/fetchContent";
|
||||
import { type AccountEnvironment } from "..";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { Header } from "./Header";
|
||||
import { MenuItem, PageNav } from "./PageNav";
|
||||
import { routes } from "../routes";
|
||||
|
||||
function mapRoutes(
|
||||
context: KeycloakContext<AccountEnvironment>,
|
||||
content: MenuItem[]
|
||||
): RouteObject[] {
|
||||
return content
|
||||
.map(item => {
|
||||
if ("children" in item) {
|
||||
return mapRoutes(context, item.children);
|
||||
}
|
||||
|
||||
// Do not add route disabled via feature flags
|
||||
if (item.isVisible && !context.environment.features[item.isVisible]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
element:
|
||||
"path" in item
|
||||
? routes.find(r => r.path === (item.id ?? item.path))?.element
|
||||
: undefined
|
||||
};
|
||||
})
|
||||
.filter(item => !!item)
|
||||
.flat();
|
||||
}
|
||||
|
||||
function CatchAllRedirect() {
|
||||
const { t } = useTranslation();
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
useEffect(() => {
|
||||
addAlert(t("pageNotFound"), AlertVariant.warning);
|
||||
}, [addAlert, t]);
|
||||
|
||||
return <Navigate to="." replace />;
|
||||
}
|
||||
|
||||
export const Root = () => {
|
||||
const context = useEnvironment<AccountEnvironment>();
|
||||
const [content, setContent] = useState<RouteObject[]>();
|
||||
|
||||
usePromise(
|
||||
signal => fetchContentJson({ signal, context }),
|
||||
content => {
|
||||
setContent([
|
||||
{
|
||||
path: decodeURIComponent(
|
||||
new URL(context.environment.baseUrl).pathname
|
||||
),
|
||||
element: (
|
||||
<Page header={<Header />} sidebar={<PageNav />} isManagedSidebar>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<div
|
||||
className="account-workspace-route"
|
||||
data-workspace="personal-identity"
|
||||
>
|
||||
<div className="account-workspace-route__content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</Page>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
...mapRoutes(context, content),
|
||||
{ path: "*", element: <CatchAllRedirect /> }
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
if (!content) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <RouterProvider router={createBrowserRouter(content)} />;
|
||||
};
|
||||
+8
-280
@@ -1,291 +1,19 @@
|
||||
:root {
|
||||
--nexus-account-void: #070b16;
|
||||
--nexus-account-chrome: #0b1120;
|
||||
--nexus-account-panel: #ffffff;
|
||||
--nexus-account-canvas: #f3f6fb;
|
||||
--nexus-account-ice: #2dc7e5;
|
||||
--nexus-account-violet: #7569f2;
|
||||
--nexus-account-text: #172033;
|
||||
--nexus-account-muted: #63708a;
|
||||
--nexus-account-line: #dce3ef;
|
||||
--pf-v5-global--primary-color--100: #007f9b;
|
||||
--pf-v5-global--primary-color--200: #00677e;
|
||||
--pf-v5-global--link--Color: #00758d;
|
||||
--pf-v5-global--link--Color--hover: #004f62;
|
||||
--pf-v5-global--BackgroundColor--100: var(--nexus-account-panel);
|
||||
--pf-v5-global--BackgroundColor--150: #f8fafc;
|
||||
--pf-v5-global--BackgroundColor--200: var(--nexus-account-canvas);
|
||||
--pf-v5-global--Color--100: var(--nexus-account-text);
|
||||
--pf-v5-global--Color--200: var(--nexus-account-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--nexus-account-line);
|
||||
--pf-v5-global--BorderColor--200: #c9d3e2;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark {
|
||||
--nexus-account-panel: #101827;
|
||||
--nexus-account-canvas: #080d18;
|
||||
--nexus-account-text: #edf3ff;
|
||||
--nexus-account-muted: #a6b2c8;
|
||||
--nexus-account-line: #2d3b52;
|
||||
--pf-v5-global--primary-color--100: #55d9ef;
|
||||
--pf-v5-global--primary-color--200: #82e5f5;
|
||||
--pf-v5-global--link--Color: #67dcef;
|
||||
--pf-v5-global--link--Color--hover: #a6effa;
|
||||
--pf-v5-global--BackgroundColor--100: var(--nexus-account-panel);
|
||||
--pf-v5-global--BackgroundColor--150: #151f30;
|
||||
--pf-v5-global--BackgroundColor--200: var(--nexus-account-canvas);
|
||||
--pf-v5-global--Color--100: var(--nexus-account-text);
|
||||
--pf-v5-global--Color--200: var(--nexus-account-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--nexus-account-line);
|
||||
--pf-v5-global--BorderColor--200: #3a4a63;
|
||||
color-scheme: dark;
|
||||
}
|
||||
/*
|
||||
* Account compatibility layer.
|
||||
* Shared colors and PatternFly primitives live in ../console-ui.css, while
|
||||
* account business composition lives in workspace.css.
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--nexus-account-text);
|
||||
background: var(--nexus-account-canvas);
|
||||
font-family: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-page,
|
||||
.pf-v5-c-page__main,
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-section.pf-m-light {
|
||||
background-color: var(--nexus-account-canvas);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main {
|
||||
position: relative;
|
||||
#root {
|
||||
isolation: isolate;
|
||||
background-image: linear-gradient(rgba(32, 62, 104, 0.028) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(32, 62, 104, 0.028) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main::before,
|
||||
.pf-v5-c-page__main::after {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
right: -12rem;
|
||||
bottom: -15rem;
|
||||
width: 34rem;
|
||||
height: 34rem;
|
||||
border: 1px solid rgba(45, 199, 229, 0.11);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main::after {
|
||||
right: -4rem;
|
||||
bottom: -7rem;
|
||||
width: 18rem;
|
||||
height: 18rem;
|
||||
border-color: rgba(117, 105, 242, 0.12);
|
||||
box-shadow: 0 0 80px rgba(45, 199, 229, 0.06);
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead {
|
||||
min-height: 68px;
|
||||
border-bottom: 1px solid rgba(99, 230, 255, 0.18);
|
||||
background: linear-gradient(90deg, rgba(99, 230, 255, 0.04), transparent 38%),
|
||||
var(--nexus-account-void);
|
||||
box-shadow: 0 8px 28px rgba(5, 9, 21, 0.18);
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 190px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-button.pf-m-link {
|
||||
color: #b9c5da;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar,
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
border-right: 1px solid rgba(117, 105, 242, 0.16);
|
||||
color: #d8e2f5;
|
||||
background: radial-gradient(
|
||||
circle at 18% 88%,
|
||||
rgba(99, 230, 255, 0.08),
|
||||
transparent 14rem
|
||||
),
|
||||
var(--nexus-account-chrome);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar-body::before {
|
||||
display: block;
|
||||
width: 3.5rem;
|
||||
height: 1px;
|
||||
margin: 1.5rem 1.25rem 0.8rem;
|
||||
background: linear-gradient(90deg, var(--nexus-account-ice), transparent);
|
||||
box-shadow: 0.8rem 0.35rem 0 -0.12rem var(--nexus-account-violet);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link,
|
||||
.pf-v5-c-nav__section-title,
|
||||
.pf-v5-c-nav__link.pf-m-expandable {
|
||||
color: #9daac3;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link {
|
||||
border-radius: 2px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover,
|
||||
.pf-v5-c-nav__link:focus {
|
||||
color: #ffffff;
|
||||
background: rgba(99, 230, 255, 0.06);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current,
|
||||
.pf-v5-c-nav__item.pf-m-current > .pf-v5-c-nav__link {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(99, 230, 255, 0.15),
|
||||
rgba(117, 105, 242, 0.08)
|
||||
);
|
||||
box-shadow: inset 2px 0 var(--nexus-account-ice);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current::after {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section:first-child {
|
||||
border-bottom: 1px solid rgba(220, 227, 239, 0.8);
|
||||
background: color-mix(in srgb, var(--nexus-account-panel) 78%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section + .pf-v5-c-page__main-section {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-data-list,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-drawer__panel,
|
||||
.pf-v5-c-menu {
|
||||
border: 1px solid var(--nexus-account-line);
|
||||
border-radius: 3px;
|
||||
background: var(--nexus-account-panel);
|
||||
box-shadow: 0 15px 42px rgba(27, 43, 72, 0.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-title,
|
||||
.pf-v5-c-content h1,
|
||||
.pf-v5-c-content h2,
|
||||
.pf-v5-c-page__main-title {
|
||||
color: var(--nexus-account-text);
|
||||
font-family: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
|
||||
letter-spacing: -0.018em;
|
||||
}
|
||||
|
||||
.pf-v5-c-title.pf-m-2xl::before {
|
||||
display: inline-block;
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
margin: 0 0.75rem 0.18rem 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--nexus-account-ice),
|
||||
var(--nexus-account-violet)
|
||||
);
|
||||
content: "";
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.pf-v5-c-button {
|
||||
border-radius: 2px;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(105deg, #007f9b, #087f91 58%, #655bd7);
|
||||
box-shadow: 0 6px 18px rgba(0, 127, 155, 0.16);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control,
|
||||
.pf-v5-c-select__toggle,
|
||||
.pf-v5-c-menu-toggle {
|
||||
border-radius: 2px;
|
||||
border-color: var(--pf-v5-global--BorderColor--200);
|
||||
color: var(--nexus-account-text);
|
||||
background: var(--nexus-account-panel);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .pf-v5-c-page__main {
|
||||
background-image: linear-gradient(rgba(99, 230, 255, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(99, 230, 255, 0.035) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .pf-v5-c-card,
|
||||
.pf-v5-theme-dark .pf-v5-c-data-list,
|
||||
.pf-v5-theme-dark .pf-v5-c-modal-box,
|
||||
.pf-v5-theme-dark .pf-v5-c-drawer__panel,
|
||||
.pf-v5-theme-dark .pf-v5-c-menu {
|
||||
box-shadow: 0 15px 42px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:focus-within,
|
||||
.pf-v5-c-menu-toggle:focus {
|
||||
border-color: #008eaa;
|
||||
box-shadow: 0 0 0 3px rgba(45, 199, 229, 0.12);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link::after {
|
||||
border-color: var(--nexus-account-ice);
|
||||
}
|
||||
|
||||
.pf-v5-c-label {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--nexus-account-ice) !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-v5-c-masthead {
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 158px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar-body::before {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/* Account workspace: self-service identity tasks share one business-page skeleton. */
|
||||
|
||||
.account-workspace-route,
|
||||
.account-workspace-route__content,
|
||||
.account-business-page {
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nexus-nav-context {
|
||||
display: grid;
|
||||
gap: 0.28rem;
|
||||
margin: 0 0.35rem 1.25rem;
|
||||
padding: 0.15rem 0.45rem 1.15rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.nexus-nav-context__eyebrow {
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nexus-nav-context strong {
|
||||
color: var(--console-ink);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 740;
|
||||
}
|
||||
|
||||
.nexus-nav-context__rule {
|
||||
width: 4.5rem;
|
||||
height: 3px;
|
||||
margin-top: 0.48rem;
|
||||
border-radius: 99px;
|
||||
background: var(--console-primary);
|
||||
}
|
||||
|
||||
.nexus-nav-label {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6rem minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.68rem;
|
||||
}
|
||||
|
||||
.nexus-nav-label__icon {
|
||||
display: grid;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 7px;
|
||||
color: var(--console-faint);
|
||||
background: color-mix(in srgb, var(--console-surface) 58%, transparent);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover .nexus-nav-label__icon,
|
||||
.pf-v5-c-nav__link:focus .nexus-nav-label__icon,
|
||||
.pf-v5-c-nav__link.pf-m-current .nexus-nav-label__icon {
|
||||
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) 9%, var(--console-surface));
|
||||
}
|
||||
|
||||
.account-view-header {
|
||||
position: relative;
|
||||
padding-top: clamp(1.9rem, 4vw, 3.7rem);
|
||||
padding-bottom: clamp(1.5rem, 2.8vw, 2.4rem);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.account-view-header::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
width: 3px;
|
||||
border-radius: 99px;
|
||||
background: var(--console-primary);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.account-view-header__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: end;
|
||||
gap: 2rem;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.account-view-header__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-view-header__eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 0.62rem;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 0.67rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.account-view-header h1 {
|
||||
margin: 0;
|
||||
color: var(--console-ink);
|
||||
font-size: clamp(2.15rem, 3.6vw, 3.35rem);
|
||||
font-weight: 740;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.account-view-header p {
|
||||
max-width: 72ch;
|
||||
margin: 0.85rem 0 0;
|
||||
color: var(--console-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.account-view-header__context {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.8rem;
|
||||
min-width: 156px;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
color: var(--console-muted);
|
||||
background: color-mix(in srgb, var(--console-surface) 78%, transparent);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.account-view-header__context span {
|
||||
font-size: 0.59rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.account-view-header__context strong {
|
||||
grid-row: span 2;
|
||||
color: var(--console-primary-deep);
|
||||
font-size: 1.45rem;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.account-view-header__context i {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(90deg, var(--console-teal), var(--console-primary));
|
||||
}
|
||||
|
||||
.account-view-body {
|
||||
padding-top: 0;
|
||||
padding-bottom: clamp(3.5rem, 7vw, 6rem);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.account-view-body__surface {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Lists, security devices, applications and permissions become operational decks. */
|
||||
.account-view-body__surface > .pf-v5-c-data-list,
|
||||
.account-view-body__surface > .pf-v5-c-table,
|
||||
.account-view-body__surface > .pf-v5-c-card,
|
||||
.account-view-body__surface > .pf-v5-c-stack,
|
||||
.account-view-body__surface > .pf-v5-c-form,
|
||||
.account-view-body__surface > form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__item-row {
|
||||
min-height: 72px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__cell {
|
||||
color: var(--console-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__cell strong,
|
||||
.account-view-body .pf-v5-c-data-list__cell b,
|
||||
.account-view-body .pf-v5-c-data-list__cell h2,
|
||||
.account-view-body .pf-v5-c-data-list__cell h3 {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-stack > .pf-v5-c-stack__item + .pf-v5-c-stack__item {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-title.pf-m-xl,
|
||||
.account-view-body .pf-v5-c-title.pf-m-lg {
|
||||
margin-bottom: 0.85rem;
|
||||
padding-bottom: 0.72rem;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form {
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form__group {
|
||||
padding-block: 0.28rem;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form__actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--console-surface) 92%, transparent);
|
||||
box-shadow: 0 -10px 28px color-mix(in srgb, var(--console-ink) 8%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
/* Personal information is a two-column identity record, not a generic form. */
|
||||
.nexus-profile-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(245px, 0.58fr) minmax(0, 1.65fr);
|
||||
align-items: start;
|
||||
gap: clamp(1.25rem, 2.6vw, 2.5rem);
|
||||
}
|
||||
|
||||
.nexus-identity-card,
|
||||
.nexus-profile-editor {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.nexus-identity-card {
|
||||
min-height: 0;
|
||||
padding: 1.45rem;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
}
|
||||
|
||||
.nexus-identity-card::before {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 4px;
|
||||
background: var(--console-primary);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nexus-identity-card__topline,
|
||||
.nexus-identity-card__name > span,
|
||||
.nexus-identity-card__facts dt,
|
||||
.nexus-identity-card__footer,
|
||||
.nexus-identity-card__name p {
|
||||
color: var(--console-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nexus-identity-card__topline i {
|
||||
border-color: var(--console-primary);
|
||||
border-radius: 50%;
|
||||
background: var(--console-primary);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--console-primary) 12%, transparent);
|
||||
}
|
||||
|
||||
.nexus-identity-card__avatar {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 1.8rem 0 1.35rem;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(145deg, var(--console-primary), var(--console-teal));
|
||||
box-shadow: 0 10px 24px color-mix(in srgb, var(--console-primary) 22%, transparent);
|
||||
}
|
||||
|
||||
.nexus-identity-card__avatar span {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.nexus-identity-card__name h2,
|
||||
.nexus-identity-card__facts dd {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.nexus-identity-card__name h2 {
|
||||
font-size: 1.55rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.nexus-identity-card__facts > div {
|
||||
border-top-color: var(--console-line);
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header {
|
||||
padding: 1.55rem clamp(1.25rem, 2.5vw, 2rem);
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header > span {
|
||||
color: var(--console-primary-deep);
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header h2 {
|
||||
color: var(--console-ink);
|
||||
font-size: 1.28rem;
|
||||
}
|
||||
|
||||
.nexus-profile-editor__header p {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.nexus-profile-editor > .pf-v5-c-form {
|
||||
padding: clamp(1.35rem, 3vw, 2.2rem);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.account-view-header::before {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.account-view-header__layout {
|
||||
grid-template-columns: 1fr;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.account-view-header__context {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.account-view-header h1 {
|
||||
font-size: 2.15rem;
|
||||
}
|
||||
|
||||
.nexus-profile-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-data-list__item-row {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.account-view-body .pf-v5-c-form__actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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"
|
||||
data-workspace="realm-operations"
|
||||
>
|
||||
<div className="admin-workspace-route__content">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</AuthWall>
|
||||
</Suspense>
|
||||
</ErrorBoundaryFallback>
|
||||
</Page>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<SessionExpirationWarningOverlay warnUserSecondsBeforeAutoLogout={45} />
|
||||
</AppContexts>
|
||||
</AdminClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -13,8 +13,15 @@ import { createHashRouter, RouterProvider } from "react-router-dom";
|
||||
import { i18n } from "./i18n/i18n";
|
||||
import { Root } from "./Root";
|
||||
import { routes } from "./routes";
|
||||
import { getKcContext } from "./KcContext";
|
||||
|
||||
import "./index.css";
|
||||
import "../console-ui.css";
|
||||
import "./workspace.css";
|
||||
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
document.documentElement.dataset.kcThemeName = kcContext.themeName;
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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/PageHeader.tsx" --revert
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
import logoSvgUrl from "./assets/logo.svg";
|
||||
import { KeycloakMasthead, useEnvironment, useHelp } from "../shared/keycloak-ui-shared";
|
||||
import { DropdownItem, ToolbarItem } from "../shared/@patternfly/react-core";
|
||||
import { HelpIcon } from "../shared/@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHref } from "react-router-dom";
|
||||
import { PageHeaderClearCachesModal } from "./PageHeaderClearCachesModal";
|
||||
import { HelpHeader } from "./components/help-enabler/HelpHeader";
|
||||
import { useAccess } from "./context/access/Access";
|
||||
import { useRealm } from "./context/realm-context/RealmContext";
|
||||
import { toDashboard } from "./dashboard/routes/Dashboard";
|
||||
import type { Environment } from "./environment-types";
|
||||
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
|
||||
import { joinPath } from "./utils/joinPath";
|
||||
import { useIsFeatureDisabled, Feature } from "./utils/useIsFeatureEnabled";
|
||||
import useToggle from "./utils/useToggle";
|
||||
import { ThemeModeToggle } from "../shared/ThemeModeToggle";
|
||||
|
||||
const ManageAccountDropdownItem = () => {
|
||||
const { keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const isFeatureDisabled = useIsFeatureDisabled();
|
||||
|
||||
if (isFeatureDisabled(Feature.AccountV3)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key="manage account"
|
||||
id="manage-account"
|
||||
onClick={() => keycloak.accountManagement()}
|
||||
>
|
||||
{t("manageAccount")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerInfoDropdownItem = () => {
|
||||
const { realm } = useRealm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
key="server info"
|
||||
component={props => <Link {...props} to={toDashboard({ realm })} />}
|
||||
>
|
||||
{t("realmInfo")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ClearCachesDropdownItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, toggleModal] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownItem key="clear caches" onClick={() => toggleModal()}>
|
||||
{t("clearCachesTitle")}
|
||||
</DropdownItem>
|
||||
{open && <PageHeaderClearCachesModal onClose={() => toggleModal()} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const HelpDropdownItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const { enabled, toggleHelp } = useHelp();
|
||||
return (
|
||||
<DropdownItem data-testId="helpIcon" icon={<HelpIcon />} onClick={toggleHelp}>
|
||||
{enabled ? t("helpEnabled") : t("helpDisabled")}
|
||||
</DropdownItem>
|
||||
);
|
||||
};
|
||||
|
||||
const kebabDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
|
||||
<ManageAccountDropdownItem key="kebab Manage Account" />,
|
||||
<ServerInfoDropdownItem key="kebab Server Info" />,
|
||||
...(isMasterRealm && isManager
|
||||
? [<ClearCachesDropdownItem key="Clear Caches" />]
|
||||
: []),
|
||||
<HelpDropdownItem key="kebab Help" />
|
||||
];
|
||||
|
||||
const userDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
|
||||
<ManageAccountDropdownItem key="Manage Account" />,
|
||||
<ServerInfoDropdownItem key="Server info" />,
|
||||
...(isMasterRealm && isManager
|
||||
? [<ClearCachesDropdownItem key="Clear Caches" />]
|
||||
: [])
|
||||
];
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment<Environment>();
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const contextLogo = usePreviewLogo();
|
||||
const customLogo = contextLogo?.logo;
|
||||
|
||||
const isMasterRealm = realm === environment.masterRealm;
|
||||
const isManager = hasAccess("manage-realm");
|
||||
|
||||
const url = useHref(toDashboard({ realm }));
|
||||
const logoUrl = environment.logoUrl ? environment.logoUrl : url;
|
||||
|
||||
return (
|
||||
<KeycloakMasthead
|
||||
data-testid="page-header"
|
||||
keycloak={keycloak}
|
||||
features={{ hasManageAccount: false }}
|
||||
brand={{
|
||||
href: logoUrl,
|
||||
src: customLogo?.trim() ? customLogo : logoSvgUrl,
|
||||
alt: t("logo"),
|
||||
className: "keycloak__pageheader_brand"
|
||||
}}
|
||||
dropdownItems={userDropdownItems(isMasterRealm, isManager)}
|
||||
kebabDropdownItems={kebabDropdownItems(isMasterRealm, isManager)}
|
||||
toolbarItems={[
|
||||
<ToolbarItem key="theme-mode">
|
||||
<ThemeModeToggle />
|
||||
</ToolbarItem>,
|
||||
<ToolbarItem
|
||||
key="help"
|
||||
align={{ default: "alignRight" }}
|
||||
visibility={{
|
||||
default: "hidden",
|
||||
md: "visible"
|
||||
}} /** the settings and help icon buttons are only visible on desktop sizes and replaced by a kebab dropdown for other sizes */
|
||||
>
|
||||
<HelpHeader />
|
||||
</ToolbarItem>
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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,
|
||||
NavExpandable,
|
||||
NavGroup,
|
||||
PageSidebar,
|
||||
PageSidebarBody
|
||||
} from "../shared/@patternfly/react-core";
|
||||
import { FormEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavLink, useLocation, 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}`}
|
||||
end={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 { pathname } = useLocation();
|
||||
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);
|
||||
const managePaths = [
|
||||
"/organizations",
|
||||
"/clients",
|
||||
"/client-scopes",
|
||||
"/roles",
|
||||
"/users",
|
||||
"/groups",
|
||||
"/sessions",
|
||||
"/events"
|
||||
];
|
||||
const configurePaths = [
|
||||
"/realm-settings",
|
||||
"/authentication",
|
||||
"/permissions",
|
||||
"/identity-providers",
|
||||
"/user-federation",
|
||||
"/workflows",
|
||||
"/page-section"
|
||||
];
|
||||
const isManageActive = managePaths.some(path => pathname.includes(path));
|
||||
const isConfigureActive = configurePaths.some(path => pathname.includes(path));
|
||||
|
||||
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 && (
|
||||
<NavExpandable
|
||||
className="admin-nav-group"
|
||||
groupId="manage"
|
||||
title={t("manage")}
|
||||
isActive={isManageActive}
|
||||
isExpanded={isManageActive}
|
||||
>
|
||||
{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" />
|
||||
</NavExpandable>
|
||||
)}
|
||||
{showConfigure && (
|
||||
<NavExpandable
|
||||
className="admin-nav-group"
|
||||
groupId="configure"
|
||||
title={t("configure")}
|
||||
isActive={isConfigureActive}
|
||||
isExpanded={isConfigureActive}
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
))}
|
||||
</NavExpandable>
|
||||
)}
|
||||
</Nav>
|
||||
<div className="admin-nav-footer">
|
||||
<span>{t("adminWorkspaceConsoleLabel")}</span>
|
||||
<Label color="blue">{t("currentRealm")}</Label>
|
||||
</div>
|
||||
</PageSidebarBody>
|
||||
</PageSidebar>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 52" role="img" aria-labelledby="title">
|
||||
<title id="title">NEXUS ID</title>
|
||||
<defs>
|
||||
<linearGradient id="signal" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#63e6ff"/>
|
||||
<stop offset="1" stop-color="#8b7cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(8 9)">
|
||||
<rect x="5" y="5" width="24" height="24" rx="1" fill="none" stroke="url(#signal)" stroke-width="1.5" transform="rotate(45 17 17)"/>
|
||||
<circle cx="17" cy="17" r="3.5" fill="#63e6ff"/>
|
||||
<circle cx="17" cy="17" r="8.5" fill="none" stroke="#63e6ff" stroke-opacity=".18"/>
|
||||
<rect width="34" height="34" rx="10" fill="#356fd6"/>
|
||||
<path d="M9 10h16M9 17h11M9 24h16" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/>
|
||||
<circle cx="24.5" cy="17" r="2.2" fill="#f0b92e"/>
|
||||
</g>
|
||||
<text x="52" y="32" fill="#f4f7ff" font-family="Bahnschrift,Segoe UI,sans-serif" font-size="20" font-weight="600" letter-spacing="3">NEXUS</text>
|
||||
<text x="151" y="32" fill="#95a2be" font-family="Bahnschrift,Segoe UI,sans-serif" font-size="18" font-weight="400" letter-spacing="2">/ ID</text>
|
||||
<text x="54" y="31" fill="#17324d" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="19" font-weight="700" letter-spacing="1.2">NEXUS</text>
|
||||
<text x="133" y="31" fill="#667a8e" font-family="Segoe UI Variable,Segoe UI,sans-serif" font-size="15" font-weight="600" letter-spacing="1">WORKSPACE</text>
|
||||
<rect x="54" y="38" width="31" height="3" rx="1.5" fill="#279477"/>
|
||||
<rect x="88" y="38" width="31" height="3" rx="1.5" fill="#e9ad38"/>
|
||||
<rect x="122" y="38" width="31" height="3" rx="1.5" fill="#e86452"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 929 B |
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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/colorScheme.ts" --revert
|
||||
*/
|
||||
|
||||
/* IMPORTANT NOTE:
|
||||
* If you modify how the light/dark mode is implemented
|
||||
* you must also update public/admin/early-color-scheme.js
|
||||
* This scrip is ran before this to avoid white flashes
|
||||
*/
|
||||
|
||||
import { getKcContext } from "./KcContext";
|
||||
import { startConsoleColorScheme } from "../shared/consoleColorScheme";
|
||||
|
||||
export function startColorSchemeManagement() {
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
startConsoleColorScheme(kcContext.darkMode !== false);
|
||||
}
|
||||
@@ -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,437 @@
|
||||
.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: var(--console-ink);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--console-surface) 0%,
|
||||
color-mix(in srgb, var(--console-primary) 8%, var(--console-surface)) 58%,
|
||||
color-mix(in srgb, var(--console-teal) 12%, var(--console-surface)) 100%
|
||||
);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(var(--console-line) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--console-line) 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-primary), var(--console-teal));
|
||||
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: var(--console-teal);
|
||||
}
|
||||
|
||||
.admin-dashboard-hero h1 {
|
||||
margin: 0;
|
||||
color: var(--console-ink);
|
||||
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: var(--console-muted);
|
||||
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: var(--console-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__meta strong {
|
||||
color: var(--console-ink);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero .admin-dashboard-status {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.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 var(--console-line);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal > span {
|
||||
color: var(--console-faint);
|
||||
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, var(--console-line-strong));
|
||||
}
|
||||
|
||||
.admin-dashboard-hero__signal strong {
|
||||
color: var(--console-primary-deep);
|
||||
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: var(--console-muted);
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero {
|
||||
color: #edf6fb;
|
||||
background: linear-gradient(135deg, #112d43, #173f5e 58%, #1c625e);
|
||||
box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero::before {
|
||||
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);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-eyebrow {
|
||||
color: #83dcc5;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero h1,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__meta strong,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__signal strong {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__copy > p,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero .admin-dashboard-status {
|
||||
color: rgba(235, 246, 255, 0.8);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__meta,
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__signal small {
|
||||
color: rgba(235, 246, 255, 0.64);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .admin-dashboard-hero__signal {
|
||||
border-left-color: rgba(255, 255, 255, 0.13);
|
||||
}
|
||||
|
||||
@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=修改協定、驗證流程或正式環境安全設定時,可查閱官方文件。
|
||||
+11
-259
@@ -1,274 +1,26 @@
|
||||
:root {
|
||||
--nexus-admin-void: #070b16;
|
||||
--nexus-admin-chrome: #0b1120;
|
||||
--nexus-admin-panel: #ffffff;
|
||||
--nexus-admin-canvas: #f3f6fb;
|
||||
--nexus-admin-ice: #2dc7e5;
|
||||
--nexus-admin-violet: #7569f2;
|
||||
--nexus-admin-text: #172033;
|
||||
--nexus-admin-muted: #63708a;
|
||||
--nexus-admin-line: #dce3ef;
|
||||
--pf-v5-global--primary-color--100: #007f9b;
|
||||
--pf-v5-global--primary-color--200: #00677e;
|
||||
--pf-v5-global--link--Color: #00758d;
|
||||
--pf-v5-global--link--Color--hover: #004f62;
|
||||
--pf-v5-global--BackgroundColor--100: var(--nexus-admin-panel);
|
||||
--pf-v5-global--BackgroundColor--150: #f7f9fc;
|
||||
--pf-v5-global--BackgroundColor--200: var(--nexus-admin-canvas);
|
||||
--pf-v5-global--Color--100: var(--nexus-admin-text);
|
||||
--pf-v5-global--Color--200: var(--nexus-admin-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--nexus-admin-line);
|
||||
--pf-v5-global--BorderColor--200: #c9d3e2;
|
||||
--pf-v5-c-page__main-section--m-light--BackgroundColor: var(--nexus-admin-canvas);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark {
|
||||
--nexus-admin-panel: #101827;
|
||||
--nexus-admin-canvas: #080d18;
|
||||
--nexus-admin-text: #edf3ff;
|
||||
--nexus-admin-muted: #a6b2c8;
|
||||
--nexus-admin-line: #2d3b52;
|
||||
--pf-v5-global--primary-color--100: #55d9ef;
|
||||
--pf-v5-global--primary-color--200: #82e5f5;
|
||||
--pf-v5-global--link--Color: #67dcef;
|
||||
--pf-v5-global--link--Color--hover: #a6effa;
|
||||
--pf-v5-global--BackgroundColor--100: var(--nexus-admin-panel);
|
||||
--pf-v5-global--BackgroundColor--150: #151f30;
|
||||
--pf-v5-global--BackgroundColor--200: var(--nexus-admin-canvas);
|
||||
--pf-v5-global--Color--100: var(--nexus-admin-text);
|
||||
--pf-v5-global--Color--200: var(--nexus-admin-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--nexus-admin-line);
|
||||
--pf-v5-global--BorderColor--200: #3a4a63;
|
||||
--pf-v5-c-page__main-section--m-light--BackgroundColor: var(--nexus-admin-canvas);
|
||||
color-scheme: dark;
|
||||
}
|
||||
/*
|
||||
* Admin compatibility layer.
|
||||
* Visual tokens and shared PatternFly composition live in ../console-ui.css.
|
||||
* Keep this file intentionally small so theme-specific legacy rules cannot
|
||||
* override the light/dark workspace palette.
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--nexus-admin-text);
|
||||
background: var(--nexus-admin-canvas);
|
||||
font-family: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-page,
|
||||
.pf-v5-c-page__main,
|
||||
.pf-v5-c-page__main-section {
|
||||
background-color: var(--nexus-admin-canvas);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main {
|
||||
background-image: linear-gradient(rgba(32, 62, 104, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(32, 62, 104, 0.025) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead {
|
||||
border-bottom: 1px solid rgba(99, 230, 255, 0.18);
|
||||
background: linear-gradient(90deg, rgba(99, 230, 255, 0.035), transparent 36%),
|
||||
var(--nexus-admin-void);
|
||||
box-shadow: 0 8px 28px rgba(5, 9, 21, 0.18);
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 190px;
|
||||
height: 38px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar,
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
border-right: 1px solid rgba(117, 105, 242, 0.16);
|
||||
color: #d8e2f5;
|
||||
background: radial-gradient(
|
||||
circle at 20% 88%,
|
||||
rgba(99, 230, 255, 0.07),
|
||||
transparent 14rem
|
||||
),
|
||||
var(--nexus-admin-chrome);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link {
|
||||
border-radius: 2px;
|
||||
color: #9daac3;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover,
|
||||
.pf-v5-c-nav__link:focus {
|
||||
color: #ffffff;
|
||||
background: rgba(99, 230, 255, 0.06);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current,
|
||||
.pf-v5-c-nav__item.pf-m-current > .pf-v5-c-nav__link {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(99, 230, 255, 0.15),
|
||||
rgba(117, 105, 242, 0.08)
|
||||
);
|
||||
box-shadow: inset 2px 0 var(--nexus-admin-ice);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current::after {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-drawer__panel,
|
||||
.pf-v5-c-popover__content,
|
||||
.pf-v5-c-menu {
|
||||
border: 1px solid var(--nexus-admin-line);
|
||||
border-radius: 3px;
|
||||
background: var(--nexus-admin-panel);
|
||||
box-shadow: 0 15px 42px rgba(27, 43, 72, 0.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-title,
|
||||
.pf-v5-c-content h1,
|
||||
.pf-v5-c-content h2,
|
||||
.pf-v5-c-page__main-title {
|
||||
color: var(--nexus-admin-text);
|
||||
font-family: "Bahnschrift SemiCondensed", "Arial Narrow", sans-serif;
|
||||
letter-spacing: -0.018em;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
border-bottom: 1px solid rgba(220, 227, 239, 0.74);
|
||||
background: color-mix(in srgb, var(--nexus-admin-panel) 72%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.pf-v5-c-button {
|
||||
border-radius: 2px;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(105deg, #007f9b, #087f91 58%, #655bd7);
|
||||
box-shadow: 0 6px 18px rgba(0, 127, 155, 0.16);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control,
|
||||
.pf-v5-c-select__toggle,
|
||||
.pf-v5-c-menu-toggle {
|
||||
border-radius: 2px;
|
||||
border-color: var(--pf-v5-global--BorderColor--200);
|
||||
color: var(--nexus-admin-text);
|
||||
background: var(--nexus-admin-panel);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:focus-within,
|
||||
.pf-v5-c-menu-toggle:focus {
|
||||
border-color: #008eaa;
|
||||
box-shadow: 0 0 0 3px rgba(45, 199, 229, 0.12);
|
||||
}
|
||||
|
||||
.pf-v5-c-table {
|
||||
border: 1px solid var(--nexus-admin-line);
|
||||
background: var(--nexus-admin-panel);
|
||||
}
|
||||
|
||||
.pf-v5-c-table thead {
|
||||
color: var(--nexus-admin-muted);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--nexus-admin-panel) 72%,
|
||||
var(--nexus-admin-canvas)
|
||||
);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.pf-v5-c-table tr:hover > * {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--nexus-admin-ice) 7%,
|
||||
var(--nexus-admin-panel)
|
||||
);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link::after {
|
||||
border-color: var(--nexus-admin-ice);
|
||||
}
|
||||
|
||||
.pf-v5-c-label {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.keycloak__form {
|
||||
max-width: 1100px;
|
||||
padding-bottom: var(--pf-v5-global--spacer--4xl);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: baseline;
|
||||
accent-color: #0087a1;
|
||||
}
|
||||
|
||||
.keycloak__form_actions {
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
margin-left: calc(-1 * var(--pf-v5-global--spacer--lg));
|
||||
padding: var(--pf-v5-global--spacer--md);
|
||||
border-top: 1px solid var(--nexus-admin-line);
|
||||
background: color-mix(in srgb, var(--nexus-admin-panel) 90%, transparent);
|
||||
box-shadow: 0 -10px 30px rgba(27, 43, 72, 0.06);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .pf-v5-c-page__main {
|
||||
background-image: linear-gradient(rgba(99, 230, 255, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(99, 230, 255, 0.035) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark .pf-v5-c-card,
|
||||
.pf-v5-theme-dark .pf-v5-c-modal-box,
|
||||
.pf-v5-theme-dark .pf-v5-c-drawer__panel,
|
||||
.pf-v5-theme-dark .pf-v5-c-popover__content,
|
||||
.pf-v5-theme-dark .pf-v5-c-menu {
|
||||
box-shadow: 0 15px 42px rgba(0, 0, 0, 0.28);
|
||||
#root {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.keycloak__section_intro__help {
|
||||
width: fit-content;
|
||||
padding: var(--pf-v5-global--spacer--md) 0 var(--pf-v5-global--spacer--lg);
|
||||
color: var(--pf-v5-global--primary-color--100);
|
||||
}
|
||||
|
||||
div.w-tc-editor {
|
||||
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--nexus-admin-ice) !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
color: var(--console-primary-deep);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
.keycloak__page_nav__nav {
|
||||
--pf-v5-c-page__sidebar--Transition: all 120ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||
--pf-v5-c-page__sidebar--BackgroundColor: var(--console-nav);
|
||||
overflow: visible !important;
|
||||
color: var(--console-ink) !important;
|
||||
background: var(--console-nav) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-page__sidebar-body {
|
||||
overflow-y: visible !important;
|
||||
color: var(--console-ink) !important;
|
||||
background: var(--console-nav) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav {
|
||||
--pf-v5-c-nav__link--Color: var(--console-muted);
|
||||
--pf-v5-c-nav__link--hover--Color: var(--console-ink);
|
||||
--pf-v5-c-nav__link--focus--Color: var(--console-ink);
|
||||
--pf-v5-c-nav__link--hover--BackgroundColor: color-mix(
|
||||
in srgb,
|
||||
var(--console-surface) 82%,
|
||||
transparent
|
||||
);
|
||||
--pf-v5-c-nav__link--focus--BackgroundColor: color-mix(
|
||||
in srgb,
|
||||
var(--console-surface) 82%,
|
||||
transparent
|
||||
);
|
||||
--pf-v5-c-nav__link--m-current--Color: var(--console-primary-deep);
|
||||
--pf-v5-c-nav__link--m-current--BackgroundColor: color-mix(
|
||||
in srgb,
|
||||
var(--console-primary) 9%,
|
||||
var(--console-surface)
|
||||
);
|
||||
color: var(--console-ink);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link,
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link.pf-m-expandable {
|
||||
color: var(--console-muted) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link:hover,
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link:focus {
|
||||
color: var(--console-ink) !important;
|
||||
}
|
||||
|
||||
.keycloak__page_nav__nav .pf-v5-c-nav__link.pf-m-current,
|
||||
.keycloak__page_nav__nav
|
||||
.pf-v5-c-nav__item:not(.pf-m-expandable).pf-m-current
|
||||
> .pf-v5-c-nav__link {
|
||||
color: var(--console-primary-deep) !important;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--console-primary) 9%,
|
||||
var(--console-surface)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.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-group > .pf-v5-c-nav__link.pf-m-expandable {
|
||||
min-height: 40px;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--console-ink);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 760;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-nav-group .pf-v5-c-nav__subnav {
|
||||
margin-top: 0.25rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.keycloak__page_nav__nav,
|
||||
.keycloak__page_nav__nav .pf-v5-c-page__sidebar-body {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
.admin-workspace-route {
|
||||
min-width: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.admin-workspace-route__content {
|
||||
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: var(--console-primary);
|
||||
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;
|
||||
}
|
||||
|
||||
/* Every administration route follows the same operational content grammar. */
|
||||
.admin-workspace-route .pf-v5-c-page__main-section:not(.admin-view-header) {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-page__main-section:not(.admin-view-header) > form,
|
||||
.admin-workspace-route
|
||||
.pf-v5-c-page__main-section:not(.admin-view-header)
|
||||
> .pf-v5-c-form,
|
||||
.admin-workspace-route
|
||||
.pf-v5-c-page__main-section:not(.admin-view-header)
|
||||
> .keycloak__form {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-card + .pf-v5-c-card,
|
||||
.admin-workspace-route .pf-v5-c-panel + .pf-v5-c-panel,
|
||||
.admin-workspace-route .pf-v5-c-alert + .pf-v5-c-card {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-description-list {
|
||||
padding: 1.15rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-description-list__term {
|
||||
color: var(--console-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-description-list__description {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-form__actions,
|
||||
.admin-workspace-route .keycloak__form_actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 6;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--console-surface) 92%, transparent);
|
||||
box-shadow: 0 -10px 28px color-mix(in srgb, var(--console-ink) 8%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-wizard {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-wizard__nav {
|
||||
border-right-color: var(--console-line);
|
||||
background: var(--console-surface-soft);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-drawer__panel {
|
||||
background: var(--console-surface-raised);
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-empty-state {
|
||||
min-height: 260px;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.admin-workspace-route .pf-v5-c-form__actions,
|
||||
.admin-workspace-route .keycloak__form_actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
/*
|
||||
* Shared account/admin workspace language.
|
||||
* This layer intentionally comes after the owned Keycloak styles so the two
|
||||
* consoles feel like one product without coupling their generated sources.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--console-ink: #1d2a33;
|
||||
--console-muted: #64727c;
|
||||
--console-faint: #87939b;
|
||||
--console-canvas: #f4f6f7;
|
||||
--console-surface: #ffffff;
|
||||
--console-surface-soft: #f8f9f9;
|
||||
--console-surface-raised: #ffffff;
|
||||
--console-surface-sunken: #edf0f1;
|
||||
--console-nav: #eef1f2;
|
||||
--console-line: #d9dfe2;
|
||||
--console-line-strong: #bac5ca;
|
||||
--console-primary: #34747a;
|
||||
--console-primary-deep: #245b61;
|
||||
--console-teal: #347f72;
|
||||
--console-amber: #d99b25;
|
||||
--console-coral: #dd5c4b;
|
||||
--console-danger: #c94046;
|
||||
--console-radius-sm: 8px;
|
||||
--console-radius: 14px;
|
||||
--console-shadow: 0 1px 2px rgba(23, 50, 77, 0.04),
|
||||
0 14px 38px rgba(23, 50, 77, 0.075);
|
||||
--pf-v5-global--primary-color--100: var(--console-primary);
|
||||
--pf-v5-global--primary-color--200: var(--console-primary-deep);
|
||||
--pf-v5-global--link--Color: var(--console-primary-deep);
|
||||
--pf-v5-global--link--Color--hover: #173f88;
|
||||
--pf-v5-global--BackgroundColor--100: var(--console-surface);
|
||||
--pf-v5-global--BackgroundColor--150: var(--console-surface-soft);
|
||||
--pf-v5-global--BackgroundColor--200: var(--console-canvas);
|
||||
--pf-v5-global--Color--100: var(--console-ink);
|
||||
--pf-v5-global--Color--200: var(--console-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--console-line);
|
||||
--pf-v5-global--BorderColor--200: var(--console-line-strong);
|
||||
--pf-v5-c-masthead--BackgroundColor: var(--console-surface);
|
||||
--pf-v5-c-masthead--c-toolbar--BackgroundColor: var(--console-surface);
|
||||
--pf-v5-c-page__sidebar--BackgroundColor: var(--console-nav);
|
||||
--pf-v5-c-page__main-section--m-light--BackgroundColor: transparent;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark {
|
||||
--console-ink: #eaf2f8;
|
||||
--console-muted: #a4b5c4;
|
||||
--console-faint: #8295a7;
|
||||
--console-canvas: #10171c;
|
||||
--console-surface: #182229;
|
||||
--console-surface-soft: #1d2930;
|
||||
--console-surface-raised: #202c34;
|
||||
--console-surface-sunken: #121b21;
|
||||
--console-nav: #141e24;
|
||||
--console-line: #304048;
|
||||
--console-line-strong: #52636b;
|
||||
--console-primary: #75adb0;
|
||||
--console-primary-deep: #93c2c4;
|
||||
--console-teal: #6aafa0;
|
||||
--console-amber: #f2c45c;
|
||||
--console-coral: #ff8173;
|
||||
--console-danger: #ff858b;
|
||||
--console-shadow: 0 18px 42px rgba(0, 0, 0, 0.26);
|
||||
--pf-v5-global--primary-color--100: var(--console-primary);
|
||||
--pf-v5-global--primary-color--200: var(--console-primary-deep);
|
||||
--pf-v5-global--link--Color: #9dbfff;
|
||||
--pf-v5-global--link--Color--hover: #c3d7ff;
|
||||
--pf-v5-global--BackgroundColor--100: var(--console-surface);
|
||||
--pf-v5-global--BackgroundColor--150: var(--console-surface-soft);
|
||||
--pf-v5-global--BackgroundColor--200: var(--console-canvas);
|
||||
--pf-v5-global--Color--100: var(--console-ink);
|
||||
--pf-v5-global--Color--200: var(--console-muted);
|
||||
--pf-v5-global--BorderColor--100: var(--console-line);
|
||||
--pf-v5-global--BorderColor--200: var(--console-line-strong);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"] {
|
||||
--console-ink: #202c33;
|
||||
--console-muted: #66757c;
|
||||
--console-canvas: #f6f7f7;
|
||||
--console-nav: #eff2f2;
|
||||
--console-surface-raised: #ffffff;
|
||||
--console-surface-sunken: #ecefef;
|
||||
--console-line: #d9dfe0;
|
||||
--console-line-strong: #b7c3c5;
|
||||
--console-primary: #3a7277;
|
||||
--console-primary-deep: #285b60;
|
||||
--console-teal: #3b7f70;
|
||||
--console-amber: #f0b92e;
|
||||
--console-coral: #ef6654;
|
||||
}
|
||||
|
||||
html[data-kc-theme-name="class"].pf-v5-theme-dark {
|
||||
--console-ink: #e7f1f9;
|
||||
--console-muted: #9ab0c2;
|
||||
--console-canvas: #11181d;
|
||||
--console-surface: #19242a;
|
||||
--console-surface-soft: #1e2b31;
|
||||
--console-nav: #151f24;
|
||||
--console-surface-raised: #222f35;
|
||||
--console-surface-sunken: #121b20;
|
||||
--console-line: #314147;
|
||||
--console-line-strong: #53656c;
|
||||
--console-primary: #78aeb0;
|
||||
--console-primary-deep: #97c4c5;
|
||||
--console-teal: #70ad9f;
|
||||
--console-amber: #f2c45c;
|
||||
--console-coral: #ff8173;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root,
|
||||
.pf-v5-c-page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--console-ink);
|
||||
background: var(--console-canvas);
|
||||
font-family: "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei",
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
html[data-console-color-mode] body,
|
||||
html[data-console-color-mode] .pf-v5-c-page,
|
||||
html[data-console-color-mode] .pf-v5-c-page__sidebar,
|
||||
html[data-console-color-mode] .pf-v5-c-masthead {
|
||||
transition:
|
||||
color 180ms ease,
|
||||
background-color 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
|
||||
.pf-v5-c-page,
|
||||
.pf-v5-c-page__main,
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-section.pf-m-light {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main {
|
||||
min-width: 0;
|
||||
overflow-x: clip;
|
||||
background-color: var(--console-canvas);
|
||||
background-image: radial-gradient(
|
||||
circle at 92% 1%,
|
||||
color-mix(in srgb, var(--console-primary) 5%, transparent),
|
||||
transparent 30rem
|
||||
);
|
||||
}
|
||||
|
||||
/* Header: quiet, bright, and consistent across both consoles. */
|
||||
.pf-v5-c-masthead {
|
||||
--pf-v5-c-masthead--BackgroundColor: var(--console-surface);
|
||||
--pf-v5-c-masthead--c-toolbar--BackgroundColor: var(--console-surface);
|
||||
min-height: 68px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
color: var(--console-ink);
|
||||
background: color-mix(in srgb, var(--console-surface) 94%, transparent) !important;
|
||||
box-shadow: 0 6px 24px rgba(23, 50, 77, 0.055);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead,
|
||||
.pf-v5-c-masthead__main,
|
||||
.pf-v5-c-masthead__content,
|
||||
.pf-v5-c-masthead .pf-v5-c-toolbar,
|
||||
.pf-v5-c-masthead .pf-v5-c-toolbar__content {
|
||||
color: var(--console-ink) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-menu-toggle {
|
||||
--pf-v5-c-menu-toggle--Color: var(--console-ink);
|
||||
--pf-v5-c-menu-toggle--BackgroundColor: var(--console-surface-soft);
|
||||
color: var(--console-ink) !important;
|
||||
background: var(--console-surface-soft) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead::after {
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
width: min(28vw, 25rem);
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--console-primary));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 180px;
|
||||
height: 40px;
|
||||
filter: drop-shadow(0 1px 1px rgba(23, 50, 77, 0.1));
|
||||
}
|
||||
|
||||
.pf-v5-theme-dark img.keycloak__pageheader_brand {
|
||||
padding: 0.2rem 0.45rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 10px;
|
||||
background: #f8fbfe;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-button.pf-m-link,
|
||||
.pf-v5-c-masthead .pf-v5-c-button.pf-m-plain {
|
||||
color: var(--console-muted);
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead .pf-v5-c-button:hover {
|
||||
color: var(--console-primary-deep);
|
||||
background: var(--console-nav);
|
||||
}
|
||||
|
||||
.console-theme-toggle.pf-v5-c-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.48rem;
|
||||
min-width: 5.15rem;
|
||||
min-height: 38px;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: 999px;
|
||||
color: var(--console-muted);
|
||||
background: var(--console-surface-soft);
|
||||
}
|
||||
|
||||
.console-theme-toggle.pf-v5-c-button:hover {
|
||||
border-color: var(--console-line-strong);
|
||||
color: var(--console-primary-deep);
|
||||
background: var(--console-surface-raised);
|
||||
}
|
||||
|
||||
.console-theme-toggle__label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Navigation is now a calm secondary surface instead of a competing dark panel. */
|
||||
.pf-v5-c-page__sidebar,
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
color: var(--console-ink);
|
||||
background: var(--console-nav) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar {
|
||||
--pf-v5-c-page__sidebar--BackgroundColor: var(--console-nav);
|
||||
width: 272px;
|
||||
border-right: 1px solid var(--console-line);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar-body {
|
||||
padding: 1.25rem 0.9rem 2rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar-body::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__section-title,
|
||||
.pf-v5-c-nav__link,
|
||||
.pf-v5-c-nav__group-title,
|
||||
.pf-v5-c-nav__link.pf-m-expandable {
|
||||
color: var(--console-muted) !important;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__section-title {
|
||||
color: var(--console-ink);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__group-title {
|
||||
padding-top: 1.35rem;
|
||||
color: var(--console-faint);
|
||||
font-family: inherit;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__list {
|
||||
gap: 0.28rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link {
|
||||
min-height: 44px;
|
||||
padding: 0.72rem 0.9rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition:
|
||||
color 140ms ease,
|
||||
background-color 140ms ease,
|
||||
border-color 140ms ease;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link:hover,
|
||||
.pf-v5-c-nav__link:focus {
|
||||
color: var(--console-ink) !important;
|
||||
border-color: color-mix(in srgb, var(--console-primary) 15%, var(--console-line));
|
||||
background: color-mix(in srgb, var(--console-surface) 82%, transparent) !important;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current,
|
||||
.pf-v5-c-nav__item:not(.pf-m-expandable).pf-m-current > .pf-v5-c-nav__link {
|
||||
color: var(--console-primary-deep) !important;
|
||||
border-color: color-mix(in srgb, var(--console-primary) 22%, var(--console-line));
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--console-primary) 9%,
|
||||
var(--console-surface)
|
||||
) !important;
|
||||
box-shadow: 0 8px 22px rgba(39, 82, 124, 0.08);
|
||||
}
|
||||
|
||||
.pf-v5-c-nav__link.pf-m-current::after {
|
||||
right: 0.8rem;
|
||||
width: 5px;
|
||||
height: 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--console-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
padding-inline: clamp(1.25rem, 3.2vw, 3.75rem);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section > *,
|
||||
.pf-v5-c-page__main-breadcrumb > * {
|
||||
width: 100%;
|
||||
max-width: 1380px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
background: color-mix(in srgb, var(--console-surface) 82%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section {
|
||||
padding-top: clamp(1.75rem, 3vw, 3rem);
|
||||
padding-bottom: clamp(3rem, 6vw, 5.5rem);
|
||||
}
|
||||
|
||||
.pf-v5-c-title,
|
||||
.pf-v5-c-content h1,
|
||||
.pf-v5-c-content h2,
|
||||
.pf-v5-c-page__main-title {
|
||||
color: var(--console-ink);
|
||||
font-family: "Segoe UI Variable Display", "Segoe UI", "PingFang SC", sans-serif;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
|
||||
.pf-v5-c-title.pf-m-2xl,
|
||||
.pf-v5-c-page__main-title {
|
||||
font-size: clamp(2rem, 3vw, 2.75rem);
|
||||
font-weight: 720;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.pf-v5-c-title.pf-m-2xl::before,
|
||||
.pf-v5-c-page__main-title::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-content > p,
|
||||
.pf-v5-c-text-content > p,
|
||||
.pf-v5-c-helper-text,
|
||||
.pf-v5-c-form__helper-text {
|
||||
color: var(--console-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* Every working surface shares the same edge, depth and spacing rhythm. */
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-data-list,
|
||||
.pf-v5-c-table,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-drawer__panel,
|
||||
.pf-v5-c-popover__content,
|
||||
.pf-v5-c-menu,
|
||||
.pf-v5-c-empty-state {
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
box-shadow: var(--console-shadow);
|
||||
}
|
||||
|
||||
.pf-v5-c-accordion,
|
||||
.pf-v5-c-panel,
|
||||
.pf-v5-c-description-list,
|
||||
.pf-v5-c-tree-view,
|
||||
.pf-v5-c-dual-list-selector,
|
||||
.pf-v5-c-wizard,
|
||||
.pf-v5-c-code-block {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.pf-v5-c-panel,
|
||||
.pf-v5-c-wizard__main,
|
||||
.pf-v5-c-drawer__content,
|
||||
.pf-v5-c-code-block {
|
||||
border-color: var(--console-line);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-table,
|
||||
.pf-v5-c-modal-box {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pf-v5-c-card__title {
|
||||
color: var(--console-ink);
|
||||
font-family: inherit;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.pf-v5-c-card__header,
|
||||
.pf-v5-c-modal-box__header {
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--console-surface),
|
||||
var(--console-surface-soft)
|
||||
);
|
||||
}
|
||||
|
||||
.pf-v5-c-table thead,
|
||||
.pf-v5-c-data-list__item:first-child {
|
||||
color: var(--console-muted);
|
||||
background: var(--console-surface-soft);
|
||||
font-family: inherit;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.055em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pf-v5-c-table tr > *,
|
||||
.pf-v5-c-data-list__item {
|
||||
border-bottom-color: var(--console-line);
|
||||
}
|
||||
|
||||
.pf-v5-c-table tbody tr:hover > *,
|
||||
.pf-v5-c-data-list__item:not(:first-child):hover {
|
||||
background: color-mix(in srgb, var(--console-primary) 4%, var(--console-surface));
|
||||
}
|
||||
|
||||
.pf-v5-c-table tbody tr:nth-child(even) > * {
|
||||
background: color-mix(in srgb, var(--console-surface-soft) 55%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-toolbar__content {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-form__label-text {
|
||||
color: var(--console-ink);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control,
|
||||
.pf-v5-c-select__toggle,
|
||||
.pf-v5-c-menu-toggle,
|
||||
.pf-v5-c-number-input,
|
||||
.pf-v5-c-search-input {
|
||||
min-height: 42px;
|
||||
border-color: var(--console-line-strong);
|
||||
border-radius: var(--console-radius-sm);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
box-shadow: 0 1px 1px rgba(23, 50, 77, 0.025);
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:hover,
|
||||
.pf-v5-c-menu-toggle:hover,
|
||||
.pf-v5-c-search-input:hover {
|
||||
border-color: color-mix(in srgb, var(--console-primary) 48%, var(--console-line));
|
||||
}
|
||||
|
||||
.pf-v5-c-form-control:focus-within,
|
||||
.pf-v5-c-menu-toggle:focus,
|
||||
.pf-v5-c-menu-toggle.pf-m-expanded,
|
||||
.pf-v5-c-search-input:focus-within {
|
||||
border-color: var(--console-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--console-primary) 15%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-button {
|
||||
min-height: 40px;
|
||||
border-radius: var(--console-radius-sm);
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary {
|
||||
color: #ffffff;
|
||||
background: var(--console-primary);
|
||||
box-shadow: 0 7px 18px color-mix(in srgb, var(--console-primary) 22%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-primary:hover {
|
||||
background: var(--console-primary-deep);
|
||||
box-shadow: 0 9px 22px color-mix(in srgb, var(--console-primary) 27%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-secondary {
|
||||
border-color: var(--console-line-strong);
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-button.pf-m-danger {
|
||||
background: var(--console-danger);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs {
|
||||
border-bottom: 1px solid var(--console-line);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__scroll-button,
|
||||
.pf-v5-c-tabs__toggle {
|
||||
color: var(--console-muted);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link {
|
||||
color: var(--console-muted);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__item.pf-m-current .pf-v5-c-tabs__link {
|
||||
color: var(--console-primary-deep);
|
||||
}
|
||||
|
||||
.pf-v5-c-tabs__link::after {
|
||||
height: 3px;
|
||||
border-color: var(--console-primary);
|
||||
}
|
||||
|
||||
.pf-v5-c-label {
|
||||
border-radius: 999px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.pf-v5-c-alert {
|
||||
border: 1px solid var(--console-line);
|
||||
border-radius: var(--console-radius);
|
||||
box-shadow: 0 8px 24px rgba(23, 50, 77, 0.06);
|
||||
}
|
||||
|
||||
.pf-v5-c-empty-state {
|
||||
border-style: dashed;
|
||||
background: color-mix(in srgb, var(--console-surface) 88%, transparent);
|
||||
}
|
||||
|
||||
.pf-v5-c-switch__input:checked ~ .pf-v5-c-switch__toggle,
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
accent-color: var(--console-primary);
|
||||
}
|
||||
|
||||
.keycloak__form {
|
||||
max-width: 1160px;
|
||||
}
|
||||
|
||||
.keycloak__form_actions {
|
||||
border-top-color: var(--console-line);
|
||||
background: color-mix(in srgb, var(--console-surface) 92%, transparent);
|
||||
box-shadow: 0 -10px 30px rgba(23, 50, 77, 0.055);
|
||||
}
|
||||
|
||||
.pf-v5-c-modal-box__body,
|
||||
.pf-v5-c-modal-box__footer,
|
||||
.pf-v5-c-menu__content,
|
||||
.pf-v5-c-popover__body,
|
||||
.pf-v5-c-tooltip__content {
|
||||
color: var(--console-ink);
|
||||
background: var(--console-surface);
|
||||
}
|
||||
|
||||
.pf-v5-c-menu__item {
|
||||
color: var(--console-ink);
|
||||
}
|
||||
|
||||
.pf-v5-c-menu__item:hover,
|
||||
.pf-v5-c-menu__item:focus {
|
||||
background: var(--console-surface-sunken);
|
||||
}
|
||||
|
||||
div.w-tc-editor {
|
||||
border-color: var(--console-line);
|
||||
border-radius: var(--console-radius-sm);
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--console-primary) !important;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pf-v5-c-masthead {
|
||||
min-height: 60px;
|
||||
gap: 0.5rem;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-masthead::after {
|
||||
width: 52vw;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__sidebar {
|
||||
width: min(88vw, 310px);
|
||||
min-width: 0;
|
||||
max-width: min(88vw, 310px);
|
||||
box-shadow: 16px 0 40px rgba(23, 50, 77, 0.16);
|
||||
}
|
||||
|
||||
img.keycloak__pageheader_brand {
|
||||
width: 156px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.console-theme-toggle.pf-v5-c-button {
|
||||
min-width: 2.5rem;
|
||||
padding-inline: 0.65rem;
|
||||
}
|
||||
|
||||
.console-theme-toggle__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section,
|
||||
.pf-v5-c-page__main-breadcrumb {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-page__main-section {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
.pf-v5-c-card,
|
||||
.pf-v5-c-data-list,
|
||||
.pf-v5-c-table,
|
||||
.pf-v5-c-modal-box,
|
||||
.pf-v5-c-empty-state {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "./@patternfly/react-core";
|
||||
import { MoonIcon, SunIcon } from "./@patternfly/react-icons";
|
||||
import {
|
||||
getConsoleColorMode,
|
||||
subscribeToConsoleColorMode,
|
||||
toggleConsoleColorMode
|
||||
} from "./consoleColorScheme";
|
||||
|
||||
function labels(isDark: boolean) {
|
||||
const isChinese = document.documentElement.lang.toLowerCase().startsWith("zh");
|
||||
|
||||
if (isChinese) {
|
||||
return {
|
||||
current: isDark ? "暗色" : "亮色",
|
||||
action: isDark ? "切换至亮色模式" : "切换至暗色模式"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
current: isDark ? "Dark" : "Light",
|
||||
action: isDark ? "Switch to light mode" : "Switch to dark mode"
|
||||
};
|
||||
}
|
||||
|
||||
export function ThemeModeToggle() {
|
||||
const [mode, setMode] = useState(getConsoleColorMode);
|
||||
|
||||
useEffect(() => subscribeToConsoleColorMode(setMode), []);
|
||||
|
||||
if (document.documentElement.dataset.consoleDarkAllowed === "false") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDark = mode === "dark";
|
||||
const copy = labels(isDark);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="plain"
|
||||
className="console-theme-toggle"
|
||||
aria-label={copy.action}
|
||||
title={copy.action}
|
||||
aria-pressed={isDark}
|
||||
icon={isDark ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleConsoleColorMode}
|
||||
>
|
||||
<span className="console-theme-toggle__label">{copy.current}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
export type ConsoleColorMode = "light" | "dark";
|
||||
|
||||
const STORAGE_KEY = "nexus-console-color-mode";
|
||||
const DARK_THEME_CLASS = "pf-v5-theme-dark";
|
||||
const COLOR_MODE_EVENT = "nexus:color-mode-change";
|
||||
|
||||
function readStoredMode(): ConsoleColorMode | undefined {
|
||||
try {
|
||||
const value = localStorage.getItem(STORAGE_KEY);
|
||||
return value === "light" || value === "dark" ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function preferredMode(): ConsoleColorMode {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function getConsoleColorMode(): ConsoleColorMode {
|
||||
return document.documentElement.classList.contains(DARK_THEME_CLASS)
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export function setConsoleColorMode(
|
||||
mode: ConsoleColorMode,
|
||||
options: { persist?: boolean } = {}
|
||||
) {
|
||||
const { persist = true } = options;
|
||||
const isDark = mode === "dark";
|
||||
const root = document.documentElement;
|
||||
|
||||
document.getElementById("root-color-scheme-style")?.remove();
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "root-color-scheme-style";
|
||||
style.textContent = `:root { color-scheme: ${mode}; }`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
root.style.removeProperty("background-color");
|
||||
root.classList.toggle(DARK_THEME_CLASS, isDark);
|
||||
root.dataset.consoleColorMode = mode;
|
||||
|
||||
if (persist) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// Storage can be unavailable in hardened browser sessions.
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(COLOR_MODE_EVENT, { detail: mode }));
|
||||
}
|
||||
|
||||
export function startConsoleColorScheme(darkModeAllowed: boolean) {
|
||||
const root = document.documentElement;
|
||||
root.dataset.consoleDarkAllowed = String(darkModeAllowed);
|
||||
|
||||
if (!darkModeAllowed) {
|
||||
setConsoleColorMode("light", { persist: false });
|
||||
return;
|
||||
}
|
||||
|
||||
setConsoleColorMode(readStoredMode() ?? preferredMode(), { persist: false });
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mediaQuery.addEventListener("change", event => {
|
||||
if (!readStoredMode()) {
|
||||
setConsoleColorMode(event.matches ? "dark" : "light", {
|
||||
persist: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleConsoleColorMode() {
|
||||
setConsoleColorMode(getConsoleColorMode() === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
export function subscribeToConsoleColorMode(listener: (mode: ConsoleColorMode) => void) {
|
||||
const handleChange = (event: Event) => {
|
||||
listener((event as CustomEvent<ConsoleColorMode>).detail);
|
||||
};
|
||||
|
||||
window.addEventListener(COLOR_MODE_EVENT, handleChange);
|
||||
return () => window.removeEventListener(COLOR_MODE_EVENT, handleChange);
|
||||
}
|
||||
Reference in New Issue
Block a user