Moved masthead to ui-shared (#28871)

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-04-18 11:16:06 +02:00 committed by GitHub
parent ddacfbdefd
commit 6a020d93f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 48 additions and 326 deletions

View file

@ -10,9 +10,7 @@ This directory contains the UIs and related libraries of the Keycloak project wr
│ └── keycloak-server # Keycloak server for local development of UIs │ └── keycloak-server # Keycloak server for local development of UIs
├── libs ├── libs
│ ├── keycloak-admin-client # Keycloak Admin Client library for Keycloak REST API │ ├── keycloak-admin-client # Keycloak Admin Client library for Keycloak REST API
│ ├── keycloak-js # Keycloak JS library for securing HTML5/JavaScript applications │ └── keycloak-js # Keycloak JS library for securing HTML5/JavaScript applications
│ └── keycloak-masthead # Keycloak Masthead library for an easy way to bring applications into the Keycloak ecosystem, allow users to access
│ # and manage security for those applications and manage authorization of resources
├── ... ├── ...
## Data processing ## Data processing

View file

@ -24,7 +24,6 @@
"i18next": "^23.11.2", "i18next": "^23.11.2",
"i18next-http-backend": "^2.5.0", "i18next-http-backend": "^2.5.0",
"keycloak-js": "workspace:*", "keycloak-js": "workspace:*",
"keycloak-masthead": "workspace:*",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -50,7 +49,6 @@
"command": "vite --host", "command": "vite --host",
"dependencies": [ "dependencies": [
"../../libs/ui-shared:build", "../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build", "../../libs/keycloak-js:build",
"../../libs/keycloak-admin-client:build" "../../libs/keycloak-admin-client:build"
] ]
@ -59,7 +57,6 @@
"command": "vite preview", "command": "vite preview",
"dependencies": [ "dependencies": [
"../../libs/ui-shared:build", "../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build", "../../libs/keycloak-js:build",
"../../libs/keycloak-admin-client:build" "../../libs/keycloak-admin-client:build"
] ]
@ -68,7 +65,6 @@
"command": "vite build", "command": "vite build",
"dependencies": [ "dependencies": [
"../../libs/ui-shared:build", "../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build", "../../libs/keycloak-js:build",
"../../libs/keycloak-admin-client:build" "../../libs/keycloak-admin-client:build"
] ]
@ -77,7 +73,6 @@
"command": "eslint .", "command": "eslint .",
"dependencies": [ "dependencies": [
"../../libs/ui-shared:build", "../../libs/ui-shared:build",
"../../libs/keycloak-masthead:build",
"../../libs/keycloak-js:build", "../../libs/keycloak-js:build",
"../../libs/keycloak-admin-client:build" "../../libs/keycloak-admin-client:build"
] ]

View file

@ -1,15 +1,8 @@
import { Button } from "@patternfly/react-core"; import { Button } from "@patternfly/react-core";
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons"; import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
import {
KeycloakMasthead,
KeycloakProvider,
Translations,
TranslationsProvider,
} from "keycloak-masthead";
import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHref } from "react-router-dom"; import { useHref } from "react-router-dom";
import { label } from "ui-shared"; import { KeycloakMasthead, label } from "ui-shared";
import { environment } from "../environment"; import { environment } from "../environment";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
@ -47,32 +40,19 @@ export const Header = () => {
// User can indicate that he wants an internal URL by starting it with "/" // User can indicate that he wants an internal URL by starting it with "/"
const indexHref = logoUrl.startsWith("/") ? internalLogoHref : logoUrl; const indexHref = logoUrl.startsWith("/") ? internalLogoHref : logoUrl;
const translations = useMemo<Translations>(
() => ({
avatar: t("avatar"),
fullName: t("fullName"),
manageAccount: t("manageAccount"),
signOut: t("signOut"),
unknownUser: t("unknownUser"),
}),
[t],
);
return ( return (
<TranslationsProvider translations={translations}> <KeycloakMasthead
<KeycloakProvider keycloak={keycloak}> keycloak={keycloak}
<KeycloakMasthead features={{ hasManageAccount: false }}
features={{ hasManageAccount: false }} showNavToggle
showNavToggle brand={{
brand={{ href: indexHref,
href: indexHref, src: joinPath(environment.resourceUrl, brandImage),
src: joinPath(environment.resourceUrl, brandImage), alt: t("logo"),
alt: t("logo"), className: style.brand,
className: style.brand, }}
}} toolbarItems={[<ReferrerLink key="link" />]}
toolbarItems={[<ReferrerLink key="link" />]} />
/>
</KeycloakProvider>
</TranslationsProvider>
); );
}; };

View file

@ -1,58 +0,0 @@
{
"name": "keycloak-masthead",
"type": "module",
"main": "./dist/keycloak-masthead.js",
"types": "./dist/keycloak-masthead.d.ts",
"exports": {
".": {
"import": "./dist/keycloak-masthead.js",
"types": "./dist/keycloak-masthead.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "wireit",
"lint": "wireit"
},
"wireit": {
"build": {
"command": "vite build",
"dependencies": [
"../../libs/keycloak-js:build"
],
"files": [
"src/**",
"package.json",
"tsconfig.json",
"vite.config.ts"
],
"output": [
"dist/**"
]
},
"lint": {
"command": "eslint .",
"dependencies": [
"../../libs/keycloak-js:build"
]
}
},
"dependencies": {
"@patternfly/react-core": "^5.2.3",
"@patternfly/react-styles": "^5.2.1",
"keycloak-js": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@vitejs/plugin-react-swc": "^3.6.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"vite": "^5.2.9",
"vite-plugin-checker": "^0.6.4",
"vite-plugin-dts": "^3.8.3"
}
}

View file

@ -1,45 +0,0 @@
import Keycloak, { type KeycloakTokenParsed } from "keycloak-js";
import {
createContext,
PropsWithChildren,
useContext,
useMemo,
useState,
} from "react";
type KeycloakProps = {
keycloak: Keycloak;
token?: KeycloakTokenParsed;
updateToken: () => void;
};
const KeycloakContext = createContext<KeycloakProps | undefined>(undefined);
export type KeycloakProviderProps = {
keycloak: Keycloak;
};
export const useKeycloak = () => useContext(KeycloakContext);
export const KeycloakProvider = ({
keycloak,
children,
}: PropsWithChildren<KeycloakProviderProps>) => {
const [token, setToken] = useState(keycloak.tokenParsed);
const context = useMemo(
() => ({
keycloak,
token,
updateToken: async () => {
await keycloak.updateToken(-1);
setToken(keycloak.tokenParsed);
},
}),
[keycloak, token],
);
return (
<KeycloakContext.Provider value={context}>
{children}
</KeycloakContext.Provider>
);
};

View file

@ -1,11 +0,0 @@
export { default as KeycloakMasthead } from "./Masthead";
// Keycloak
export { KeycloakProvider, useKeycloak } from "./KeycloakContext";
export type { KeycloakProviderProps } from "./KeycloakContext";
// Translation
export { defaultTranslations } from "./translation/translations";
export type { Translations } from "./translation/translations";
export { TranslationsProvider } from "./translation/TranslationsContext";
export type { TranslationsProviderProps } from "./translation/TranslationsContext";

View file

@ -1,4 +0,0 @@
declare module "*.svg" {
const content: React.FC<React.SVGProps<SVGElement>>;
export default content;
}

View file

@ -1,23 +0,0 @@
import { createContext, PropsWithChildren, useContext } from "react";
import type { Translations } from "./translations";
import { defaultTranslations } from "./translations";
const TranslationsContext = createContext<Translations>(defaultTranslations);
export const useTranslations = () => useContext(TranslationsContext);
export type TranslationsProviderProps = {
translations: Translations;
};
export const TranslationsProvider = ({
translations,
children,
}: PropsWithChildren<TranslationsProviderProps>) => {
return (
<TranslationsContext.Provider value={translations}>
{children}
</TranslationsContext.Provider>
);
};

View file

@ -1,9 +0,0 @@
export const defaultTranslations = {
avatar: "Avatar",
fullName: "{{givenName}} {{familyName}}",
manageAccount: "Manage account",
signOut: "Sign out",
unknownUser: "Anonymous",
};
export type Translations = typeof defaultTranslations;

View file

@ -1,30 +0,0 @@
import { useCallback, useMemo } from "react";
import { Translations } from "./translations";
import { useTranslations } from "./TranslationsContext";
export type TranslateFunction = (
key: keyof Translations,
args?: Record<string, string>,
) => Translations[typeof key];
export const useTranslation = () => {
const translations = useTranslations();
const translate = useCallback<TranslateFunction>(
(key, args) => {
const translation = translations[key];
if (!args) {
return translation;
}
return Object.entries(args).reduce(
(formatted, [key, value]) => formatted.replaceAll(`{{${key}}}`, value),
translation,
);
},
[translations],
);
return useMemo(() => ({ t: translate }), [translate]);
};

View file

@ -1,22 +0,0 @@
import { type KeycloakTokenParsed } from "keycloak-js";
import { TranslateFunction } from "./translation/useTranslation";
export function loggedInUserName(
token: KeycloakTokenParsed | undefined,
t: TranslateFunction,
) {
if (!token) {
return t("unknownUser");
}
const givenName = token.given_name;
const familyName = token.family_name;
const preferredUsername = token.preferred_username;
if (givenName && familyName) {
return t("fullName", { givenName, familyName });
}
return givenName || familyName || preferredUsername || t("unknownUser");
}

View file

@ -1,5 +0,0 @@
{
"extends": "../../../tsconfig.json",
"include": ["src"],
"exclude": ["**/*.test.ts", "**/*.test.tsx"]
}

View file

@ -1,29 +0,0 @@
import react from "@vitejs/plugin-react-swc";
import path from "node:path";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import { defineConfig } from "vite";
import { checker } from "vite-plugin-checker";
import dts from "vite-plugin-dts";
// https://vitejs.dev/config/
export default defineConfig({
build: {
target: "esnext",
lib: {
entry: path.resolve(__dirname, "src/main.ts"),
formats: ["es"],
},
rollupOptions: {
plugins: [
peerDepsExternal({
includeDependencies: true,
}),
],
},
},
plugins: [
react(),
checker({ typescript: true }),
dts({ insertTypesEntry: true }),
],
});

View file

@ -43,7 +43,9 @@
"@keycloak/keycloak-admin-client": "workspace:*", "@keycloak/keycloak-admin-client": "workspace:*",
"@patternfly/react-core": "^5.2.3", "@patternfly/react-core": "^5.2.3",
"@patternfly/react-icons": "^5.2.1", "@patternfly/react-icons": "^5.2.1",
"@patternfly/react-styles": "^5.2.1",
"i18next": "^23.11.2", "i18next": "^23.11.2",
"keycloak-js": "workspace:*",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View file

@ -37,3 +37,4 @@ export { createNamedContext } from "./utils/createNamedContext";
export { isDefined } from "./utils/isDefined"; export { isDefined } from "./utils/isDefined";
export { useRequiredContext } from "./utils/useRequiredContext"; export { useRequiredContext } from "./utils/useRequiredContext";
export { useStoredState } from "./utils/useStoredState"; export { useStoredState } from "./utils/useStoredState";
export { default as KeycloakMasthead } from "./masthead/Masthead";

View file

@ -7,19 +7,38 @@ import {
PageHeaderToolsGroup, PageHeaderToolsGroup,
PageHeaderToolsItem, PageHeaderToolsItem,
} from "@patternfly/react-core/deprecated"; } from "@patternfly/react-core/deprecated";
import { TFunction } from "i18next";
import Keycloak, { type KeycloakTokenParsed } from "keycloak-js";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { KeycloakDropdown } from "./KeycloakDropdown";
import { useTranslation } from "./translation/useTranslation";
import { loggedInUserName } from "./util";
import { DefaultAvatar } from "./DefaultAvatar"; import { DefaultAvatar } from "./DefaultAvatar";
import { useKeycloak } from "./KeycloakContext"; import { KeycloakDropdown } from "./KeycloakDropdown";
function loggedInUserName(
token: KeycloakTokenParsed | undefined,
t: TFunction,
) {
if (!token) {
return t("unknownUser");
}
const givenName = token.given_name;
const familyName = token.family_name;
const preferredUsername = token.preferred_username;
if (givenName && familyName) {
return t("fullName", { givenName, familyName });
}
return givenName || familyName || preferredUsername || t("unknownUser");
}
type BrandLogo = BrandProps & { type BrandLogo = BrandProps & {
href: string; href: string;
}; };
type KeycloakMastheadProps = PageHeaderProps & { type KeycloakMastheadProps = PageHeaderProps & {
keycloak: Keycloak;
brand: BrandLogo; brand: BrandLogo;
avatar?: AvatarProps; avatar?: AvatarProps;
features?: { features?: {
@ -33,6 +52,7 @@ type KeycloakMastheadProps = PageHeaderProps & {
}; };
const KeycloakMasthead = ({ const KeycloakMasthead = ({
keycloak,
brand: { href: brandHref, ...brandProps }, brand: { href: brandHref, ...brandProps },
avatar, avatar,
features: { features: {
@ -46,7 +66,6 @@ const KeycloakMasthead = ({
...rest ...rest
}: KeycloakMastheadProps) => { }: KeycloakMastheadProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { keycloak } = useKeycloak()!;
const extraItems = []; const extraItems = [];
if (hasManageAccount) { if (hasManageAccount) {
extraItems.push( extraItems.push(

View file

@ -86,9 +86,6 @@ importers:
keycloak-js: keycloak-js:
specifier: workspace:* specifier: workspace:*
version: link:../../libs/keycloak-js version: link:../../libs/keycloak-js
keycloak-masthead:
specifier: workspace:*
version: link:../../libs/keycloak-masthead
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@ -388,46 +385,6 @@ importers:
specifier: ^0.3.4 specifier: ^0.3.4
version: 0.3.4 version: 0.3.4
js/libs/keycloak-masthead:
dependencies:
'@patternfly/react-core':
specifier: ^5.2.3
version: 5.2.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@patternfly/react-styles':
specifier: ^5.2.1
version: 5.2.1
keycloak-js:
specifier: workspace:*
version: link:../keycloak-js
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
devDependencies:
'@types/react':
specifier: ^18.2.79
version: 18.2.79
'@types/react-dom':
specifier: ^18.2.25
version: 18.2.25
'@vitejs/plugin-react-swc':
specifier: ^3.6.0
version: 3.6.0(vite@5.2.9(@types/node@20.12.7)(lightningcss@1.24.1)(terser@5.30.3))
rollup-plugin-peer-deps-external:
specifier: ^2.2.4
version: 2.2.4(rollup@4.14.3)
vite:
specifier: ^5.2.9
version: 5.2.9(@types/node@20.12.7)(lightningcss@1.24.1)(terser@5.30.3)
vite-plugin-checker:
specifier: ^0.6.4
version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.4.5)(vite@5.2.9(@types/node@20.12.7)(lightningcss@1.24.1)(terser@5.30.3))(vue-tsc@1.8.27(typescript@5.4.5))
vite-plugin-dts:
specifier: ^3.8.3
version: 3.8.3(@types/node@20.12.7)(rollup@4.14.3)(typescript@5.4.5)(vite@5.2.9(@types/node@20.12.7)(lightningcss@1.24.1)(terser@5.30.3))
js/libs/ui-shared: js/libs/ui-shared:
dependencies: dependencies:
'@keycloak/keycloak-admin-client': '@keycloak/keycloak-admin-client':
@ -439,9 +396,15 @@ importers:
'@patternfly/react-icons': '@patternfly/react-icons':
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 5.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@patternfly/react-styles':
specifier: ^5.2.1
version: 5.2.1
i18next: i18next:
specifier: ^23.11.2 specifier: ^23.11.2
version: 23.11.2 version: 23.11.2
keycloak-js:
specifier: workspace:*
version: link:../keycloak-js
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21