Initial version of the Account UI (#3410)
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
9ec757cae1
commit
dc3c08aa59
30 changed files with 2395 additions and 6146 deletions
4
apps/account-ui/.gitignore
vendored
Normal file
4
apps/account-ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Vite
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
63
apps/account-ui/index.html
Normal file
63
apps/account-ui/index.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<base href="./" />
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Web site to manage keycloak" />
|
||||
<title>Keycloak account console</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body, #app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.keycloak__loading-container {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
z-index: 1000;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
padding-top: 32px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="container">
|
||||
<div class="keycloak__loading-container">
|
||||
<span class="pf-c-spinner pf-m-xl" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
<div>
|
||||
<p id="loading-text">Loading the account console</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
28
apps/account-ui/package.json
Normal file
28
apps/account-ui/package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "account-ui",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext js,jsx,mjs,ts,tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@patternfly/patternfly": "^4.215.1",
|
||||
"@patternfly/react-core": "^4.239.0",
|
||||
"@patternfly/react-icons": "^4.90.0",
|
||||
"i18next": "^21.9.2",
|
||||
"i18next-http-backend": "^1.4.4",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-router": "^6.4.1",
|
||||
"react-router-dom": "^6.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.45",
|
||||
"@types/react-dom": "^17.0.16",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"vite": "^3.1.4",
|
||||
"vite-plugin-checker": "^0.5.1"
|
||||
}
|
||||
}
|
63
apps/account-ui/public/favicon.svg
Normal file
63
apps/account-ui/public/favicon.svg
Normal file
|
@ -0,0 +1,63 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs id="defs10">
|
||||
<clipPath id="clip-path">
|
||||
<path class="cls-1" id="rect4" d="M-1018.62 565.7H862.62v1175.78h-1881.24z" />
|
||||
</clipPath>
|
||||
<clipPath id="clip-path-2">
|
||||
<path class="cls-1" id="rect7" d="M0 0h512v512H0z" />
|
||||
</clipPath>
|
||||
<style id="style2">.cls-1{fill:none}.cls-24{fill:#d0d0d0}.cls-26{fill:#d9d9d9}.cls-28{fill:#d8d8d8}.cls-29{fill:#e2e2e2}.cls-31{fill:#dedede}.cls-36{fill:#00b8e3}.cls-37{fill:#33c6e9}.cls-38{fill:#008aaa}</style>
|
||||
</defs>
|
||||
<g clip-path="url(#clip-path)" id="g36" stroke-width="1.51">
|
||||
<path d="M-42.82 358l245 24.8 199.4 2z" id="path14" fill="#b17c81" stroke="#b17c81" />
|
||||
<path d="M-42.82 358l444.44 26.79 227.18-2z" id="path16" fill="#a2747c" stroke="#a2747c" />
|
||||
<path d="M401.62 384.74l163.69 138.89 63.49-140.87z" id="path18" fill="#996976" stroke="#996976" />
|
||||
<path d="M202.22 382.76l54.56 14.88 144.84-12.9z" id="path20" fill="#aa787e" stroke="#aa787e" />
|
||||
<path d="M401.62 384.74L356 537.52l209.32-13.89z" id="path22" fill="#b2777e" stroke="#b2777e" />
|
||||
<path d="M256.78 397.64L356 537.52l45.63-152.78z" id="path24" fill="#b27a7f" stroke="#b27a7f" />
|
||||
<path d="M256.78 397.64l-92.26 135.91 191.47 4z" id="path26" fill="#c78485" stroke="#c78485" />
|
||||
<path d="M202.22 382.76l-37.7 150.79 92.26-135.91z" id="path28" fill="#c08184" stroke="#c08184" />
|
||||
<path d="M-42.82 358l207.34 175.55 37.7-150.79z" id="path30" fill="#c48485" stroke="#c48485" />
|
||||
<path d="M-42.82 358l-51.59 137.9 258.93 37.7z" id="path32" fill="#d58b88" stroke="#d58b88" />
|
||||
<path d="M-94.41 495.85L-33.89 598l198.41-64.48z" id="path34" fill="#e09790" stroke="#e09790" />
|
||||
</g>
|
||||
<g clip-path="url(#clip-path-2)" id="g110">
|
||||
<path d="M438.48 152a3.79 3.79 0 01-3.32-1.89L377.39 49.94a3.91 3.91 0 00-3.39-1.89H138.33a3.79 3.79 0 00-3.33 1.89L75 153.89l-55.83 100.2a3.88 3.88 0 000 3.82L75 358l60 104a3.79 3.79 0 003.32 1.89H374a3.91 3.91 0 003.36-1.89l57.84-100.1a3.79 3.79 0 013.32-1.89h71.93a4.32 4.32 0 004.32-4.32V156.32a4.32 4.32 0 00-4.32-4.32h-72z" id="path38" fill="#4d4d4d" />
|
||||
<path d="M72.85 157.64l-55.191 98.369 5.871 12.931 54.845 89.592L114.19 360l287.27-.02H461l38.264-9.68 15.496-42.81.01-49.36v-45.72L510.46 152h-71.98l-22.11.01H147.94l-75.09 5.63" id="path27674" fill="#e2e2e2" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-opacity="1" fill-opacity="1" />
|
||||
<path class="cls-1" d="M510.46 152H78.34a3.91 3.91 0 00-3.34 1.89v.07l-2.14 3.69-26.45 45.83-29.23 50.63a3.8 3.8 0 000 3.83l6.35 11L75 358.06a3.84 3.84 0 003.34 1.94h432.18a4.27 4.27 0 004.24-4.28V156.34a4.32 4.32 0 00-4.3-4.34z" id="path40" />
|
||||
<path d="M88.1 245.5l-64.57 23.44-6.35-11a3.8 3.8 0 010-3.83l29.23-50.63z" id="path42" fill="#e1e1e1" />
|
||||
<path id="polygon44" fill="#c8c8c8" d="M472.21 264.21l42.56-6.08v49.36l-42.56-43.28z" />
|
||||
<path d="M472.21 264.21l42.55 43.28v48.21a4.27 4.27 0 01-4.24 4.28H461z" id="path46" fill="#c2c2c2" />
|
||||
<path id="polygon48" fill="#c7c7c7" d="M472.21 264.21L461 359.98h-59.54l-18.04-43.45 88.79-52.32z" />
|
||||
<path id="polygon50" fill="#cecece" d="M472.21 264.21l42.56-51.8v45.72l-42.56 6.08z" />
|
||||
<path d="M514.77 156.33v56.08l-42.55 51.8L440.12 152h70.33a4.32 4.32 0 014.32 4.33z" id="path52" fill="#d3d3d3" />
|
||||
<path id="polygon54" fill="#c6c6c6" d="M401.46 359.98h-31.4l-8.14-11.67 21.5-31.78 18.04 43.45z" />
|
||||
<path id="polygon56" fill="#d5d5d5" d="M472.21 264.21l-117.79-49.79 61.95-62.41h23.75l32.09 112.2z" />
|
||||
<path class="cls-24" d="M354.42 214.42l29 102.11 88.8-52.32z" id="path58" />
|
||||
<path id="polygon60" fill="#bfbfbf" d="M370.06 359.98h-8.52l.38-11.67 8.14 11.67z" />
|
||||
<path class="cls-26" id="polygon62" d="M416.37 152.01l-61.95 62.41-11.18-55.82 23.92-6.59h49.21z" />
|
||||
<path d="M354.42 214.42l-143 33 150.5 100.89z" id="path64" fill="#d4d4d4" />
|
||||
<path class="cls-24" d="M354.42 214.42l7.49 133.9 21.5-31.78z" id="path66" />
|
||||
<path class="cls-26" d="M343.24 158.6l-131.77 88.79 143-33z" id="path68" />
|
||||
<path class="cls-28" id="polygon70" d="M211.47 247.39L149.5 359.98h-35.31L88.1 245.5l123.37 1.89z" />
|
||||
<path class="cls-29" d="M147.94 152L88.1 245.5l-15.25-87.86 2.15-3.7v-.07a3.91 3.91 0 013.33-1.87h69.61z" id="path72" />
|
||||
<path class="cls-28" d="M114.19 360H78.33a3.84 3.84 0 01-3.33-2l-51.47-89.06L88.1 245.5z" id="path74" />
|
||||
<path id="polygon76" fill="#e4e4e4" d="M46.41 203.47l26.44-45.83L88.1 245.5z" />
|
||||
<path class="cls-31" id="polygon78" d="M276.77 152.01H172.39l39.08 95.38 131.77-88.79-39.72-6.59h-26.75z" />
|
||||
<path class="cls-31" id="polygon80" d="M156.09 152.01h-8.15L88.1 245.5l123.37 1.89-39.08-95.38h-16.3z" />
|
||||
<path id="polygon82" fill="#c5c5c5" d="M333.23 359.98h28.31l.38-11.67-28.69 11.67z" />
|
||||
<path class="cls-24" id="polygon84" d="M361.92 348.31L211.47 247.39l27.1 112.59h94.66l28.69-11.67z" />
|
||||
<path id="polygon86" fill="#d1d1d1" d="M149.5 359.98H238.57l-27.1-112.59-61.97 112.59z" />
|
||||
<path id="polygon88" fill="#ddd" d="M343.65 152.01l-.41 6.59 23.92-6.59H343.65z" />
|
||||
<path id="polygon90" fill="#e3e3e3" d="M303.52 152.01l39.72 6.59-3.66-6.59h-36.06z" />
|
||||
<path class="cls-29" id="polygon92" d="M339.58 152.01l3.66 6.59.41-6.59h-4.07z" />
|
||||
<path class="cls-36" d="M235.15 153.81L177 254.46a3.38 3.38 0 00-.42 1.64h-40.51l79.74-138.18a3.14 3.14 0 011.19 1.15l.11.11 18.08 31.41a3.49 3.49 0 01-.04 3.22z" id="path94" />
|
||||
<path class="cls-37" d="M235.08 361.89l-18 31.27a3.51 3.51 0 01-1.22 1.15L136 256.14h40.6a3.09 3.09 0 00.38 1.57.37.37 0 00.07.17l58 100.58a3.41 3.41 0 01.03 3.43z" id="path96" />
|
||||
<path class="cls-38" d="M215.81 117.92L136.07 256.1l-20 34.66-19.1-33.12a3.09 3.09 0 01-.38-1.57 3.38 3.38 0 01.42-1.64l19.3-33.43 58.75-101.74a3.4 3.4 0 013-1.75h36.04a3.58 3.58 0 011.71.41z" id="path98" />
|
||||
<path class="cls-36" d="M215.81 394.31a3.58 3.58 0 01-1.71.45H178a3.4 3.4 0 01-3-1.75l-53.72-93-5.28-9.22 20-34.66z" id="path100" />
|
||||
<path class="cls-38" d="M376.19 256.1l-79.8 138.21a3.73 3.73 0 01-1.19-1.15l-.07-.1L277 361.72a3.49 3.49 0 010-3.22l58.06-100.65a3.38 3.38 0 00.49-1.75h40.57z" id="path102" />
|
||||
<path class="cls-36" d="M415.68 256.1a3.38 3.38 0 01-.49 1.75l-78.13 135.31a3.42 3.42 0 01-2.9 1.61h-36a3.72 3.72 0 01-1.75-.45l79.78-138.22 20-34.62 19 32.91a3.35 3.35 0 01.49 1.71z" id="path104" />
|
||||
<path class="cls-36" d="M376.19 256.1h-40.56a3.35 3.35 0 00-.49-1.71l-58-100.55a3.41 3.41 0 010-3.46l18.08-31.3a3.73 3.73 0 011.19-1.15z" id="path106" />
|
||||
<path class="cls-37" d="M396.2 221.44l-20 34.62-79.81-138.14a3.72 3.72 0 011.75-.45h36a3.42 3.42 0 012.9 1.61z" id="path108" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
16
apps/account-ui/public/locales/en/translation.json
Normal file
16
apps/account-ui/public/locales/en/translation.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"accountSecurity": "Account security",
|
||||
"applications": "Applications",
|
||||
"deviceActivity": "Device activity",
|
||||
"globalNavigation": "Global navigation",
|
||||
"groups": "Groups",
|
||||
"linkedAccounts": "Linked accounts",
|
||||
"logo": "Logo",
|
||||
"personalInfo": "Personal info",
|
||||
"resources": "Resources",
|
||||
"signingIn": "Signing in",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"somethingWentWrongDescription": "Sorry, an unexpected error has occurred.",
|
||||
"tryAgain": "Try again",
|
||||
"welcomeMessage": "Welcome to Keycloak Account Management."
|
||||
}
|
1
apps/account-ui/public/logo.svg
Normal file
1
apps/account-ui/public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
7
apps/account-ui/src/account-security/DeviceActivity.tsx
Normal file
7
apps/account-ui/src/account-security/DeviceActivity.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const DeviceActivity = () => (
|
||||
<PageSection>This is the device activity page.</PageSection>
|
||||
);
|
||||
|
||||
export default DeviceActivity;
|
7
apps/account-ui/src/account-security/LinkedAccounts.tsx
Normal file
7
apps/account-ui/src/account-security/LinkedAccounts.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const LinkedAccounts = () => (
|
||||
<PageSection>This is the linked accounts page.</PageSection>
|
||||
);
|
||||
|
||||
export default LinkedAccounts;
|
5
apps/account-ui/src/account-security/SigningIn.tsx
Normal file
5
apps/account-ui/src/account-security/SigningIn.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const SigningIn = () => <PageSection>This is the signing in page.</PageSection>;
|
||||
|
||||
export default SigningIn;
|
7
apps/account-ui/src/applications/Applications.tsx
Normal file
7
apps/account-ui/src/applications/Applications.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const Applications = () => (
|
||||
<PageSection>This is the applications page.</PageSection>
|
||||
);
|
||||
|
||||
export default Applications;
|
11
apps/account-ui/src/environment.ts
Normal file
11
apps/account-ui/src/environment.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export type Environment = {
|
||||
/** The URL to resources such as the files in the `public` directory. */
|
||||
resourceUrl: string;
|
||||
};
|
||||
|
||||
// The default environment, used during development.
|
||||
const defaultEnvironment: Environment = {
|
||||
resourceUrl: "http://localhost:8080",
|
||||
};
|
||||
|
||||
export { defaultEnvironment as environment };
|
5
apps/account-ui/src/groups/Groups.tsx
Normal file
5
apps/account-ui/src/groups/Groups.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const Groups = () => <PageSection>This is the groups page.</PageSection>;
|
||||
|
||||
export default Groups;
|
23
apps/account-ui/src/i18n.ts
Normal file
23
apps/account-ui/src/i18n.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { createInstance } from "i18next";
|
||||
import HttpBackend from "i18next-http-backend";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import { environment } from "./environment";
|
||||
|
||||
const DEFAULT_LOCALE = "en";
|
||||
const DEFAULT_NAMESPACE = "translation";
|
||||
|
||||
export const i18n = createInstance({
|
||||
defaultNS: DEFAULT_NAMESPACE,
|
||||
fallbackLng: DEFAULT_LOCALE,
|
||||
ns: [DEFAULT_NAMESPACE],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
loadPath: environment.resourceUrl + "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
});
|
||||
|
||||
i18n.use(HttpBackend);
|
||||
i18n.use(initReactI18next);
|
19
apps/account-ui/src/main.tsx
Normal file
19
apps/account-ui/src/main.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import "@patternfly/react-core/dist/styles/base.css";
|
||||
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
import { i18n } from "./i18n";
|
||||
import { routes } from "./routes";
|
||||
|
||||
await i18n.init();
|
||||
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
ReactDOM.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
document.getElementById("app")
|
||||
);
|
7
apps/account-ui/src/personal-info/PersonalInfo.tsx
Normal file
7
apps/account-ui/src/personal-info/PersonalInfo.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const PersonalInfo = () => (
|
||||
<PageSection>This is the personal info page.</PageSection>
|
||||
);
|
||||
|
||||
export default PersonalInfo;
|
12
apps/account-ui/src/react-i18next.d.ts
vendored
Normal file
12
apps/account-ui/src/react-i18next.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import "react-i18next";
|
||||
|
||||
import translation from "../public/locales/en/translation.json";
|
||||
|
||||
declare module "react-i18next" {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: "translation";
|
||||
resources: {
|
||||
translation: typeof translation;
|
||||
};
|
||||
}
|
||||
}
|
5
apps/account-ui/src/resources/Resources.tsx
Normal file
5
apps/account-ui/src/resources/Resources.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
|
||||
const Resources = () => <PageSection>This is the resources page.</PageSection>;
|
||||
|
||||
export default Resources;
|
57
apps/account-ui/src/root/ErrorPage.tsx
Normal file
57
apps/account-ui/src/root/ErrorPage.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Page,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouteError } from "react-router";
|
||||
|
||||
export const ErrorPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const error = useRouteError();
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
function onRetry() {
|
||||
location.href = location.origin + location.pathname;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("somethingWentWrong")}
|
||||
titleIconVariant="danger"
|
||||
showClose={false}
|
||||
isOpen
|
||||
actions={[
|
||||
<Button key="tryAgain" variant="primary" onClick={onRetry}>
|
||||
{t("tryAgain")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<TextContent>
|
||||
<Text>{t("somethingWentWrongDescription")}</Text>
|
||||
{errorMessage && (
|
||||
<Text component={TextVariants.small}>{errorMessage}</Text>
|
||||
)}
|
||||
</TextContent>
|
||||
</Modal>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
function getErrorMessage(error: unknown) {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
3
apps/account-ui/src/root/PageHeader.module.css
Normal file
3
apps/account-ui/src/root/PageHeader.module.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.brand {
|
||||
height: 35px;
|
||||
}
|
39
apps/account-ui/src/root/PageHeader.tsx
Normal file
39
apps/account-ui/src/root/PageHeader.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
Brand,
|
||||
Masthead,
|
||||
MastheadBrand,
|
||||
MastheadMain,
|
||||
MastheadToggle,
|
||||
PageToggleButton,
|
||||
} from "@patternfly/react-core";
|
||||
import { BarsIcon } from "@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHref, useLinkClickHandler } from "react-router-dom";
|
||||
|
||||
import { environment } from "../environment";
|
||||
import classes from "./PageHeader.module.css";
|
||||
|
||||
export const PageHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
const href = useHref("/");
|
||||
const handleClick = useLinkClickHandler("/");
|
||||
|
||||
return (
|
||||
<Masthead>
|
||||
<MastheadToggle>
|
||||
<PageToggleButton variant="plain" aria-label={t("globalNavigation")}>
|
||||
<BarsIcon />
|
||||
</PageToggleButton>
|
||||
</MastheadToggle>
|
||||
<MastheadMain>
|
||||
<MastheadBrand href={href} onClick={handleClick}>
|
||||
<Brand
|
||||
className={classes.brand}
|
||||
src={environment.resourceUrl + "/logo.svg"}
|
||||
alt={t("logo")}
|
||||
/>
|
||||
</MastheadBrand>
|
||||
</MastheadMain>
|
||||
</Masthead>
|
||||
);
|
||||
};
|
150
apps/account-ui/src/root/PageNav.tsx
Normal file
150
apps/account-ui/src/root/PageNav.tsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import {
|
||||
Nav,
|
||||
NavExpandable,
|
||||
NavItem,
|
||||
NavList,
|
||||
PageSidebar,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
FunctionComponent,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { TFuncKey, useTranslation } from "react-i18next";
|
||||
import {
|
||||
matchPath,
|
||||
To,
|
||||
useHref,
|
||||
useLinkClickHandler,
|
||||
useLocation,
|
||||
} from "react-router-dom";
|
||||
|
||||
type RootMenuItem = {
|
||||
label: TFuncKey;
|
||||
path: string;
|
||||
};
|
||||
|
||||
type MenuItemWithChildren = {
|
||||
label: TFuncKey;
|
||||
children: MenuItem[];
|
||||
};
|
||||
|
||||
type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
label: "personalInfo",
|
||||
path: "personal-info",
|
||||
},
|
||||
{
|
||||
label: "accountSecurity",
|
||||
children: [
|
||||
{
|
||||
label: "signingIn",
|
||||
path: "account-security/signing-in",
|
||||
},
|
||||
{
|
||||
label: "deviceActivity",
|
||||
path: "account-security/device-activity",
|
||||
},
|
||||
{
|
||||
label: "linkedAccounts",
|
||||
path: "account-security/linked-accounts",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "applications",
|
||||
path: "applications",
|
||||
},
|
||||
{
|
||||
label: "groups",
|
||||
path: "groups",
|
||||
},
|
||||
{
|
||||
label: "resources",
|
||||
path: "resources",
|
||||
},
|
||||
];
|
||||
|
||||
export const PageNav = () => (
|
||||
<PageSidebar
|
||||
nav={
|
||||
<Nav>
|
||||
<NavList>
|
||||
{menuItems.map((menuItem) => (
|
||||
<NavMenuItem key={menuItem.label} menuItem={menuItem} />
|
||||
))}
|
||||
</NavList>
|
||||
</Nav>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
type NavMenuItemProps = {
|
||||
menuItem: MenuItem;
|
||||
};
|
||||
|
||||
function NavMenuItem({ menuItem }: NavMenuItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const { pathname } = useLocation();
|
||||
const isActive = useMemo(
|
||||
() => matchMenuItem(pathname, menuItem),
|
||||
[pathname, menuItem]
|
||||
);
|
||||
|
||||
if ("path" in menuItem) {
|
||||
return (
|
||||
<NavLink to={menuItem.path} isActive={isActive}>
|
||||
{t(menuItem.label)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavExpandable
|
||||
title={t(menuItem.label)}
|
||||
isActive={isActive}
|
||||
isExpanded={isActive}
|
||||
>
|
||||
{menuItem.children.map((child) => (
|
||||
<NavMenuItem key={child.label} menuItem={child} />
|
||||
))}
|
||||
</NavExpandable>
|
||||
);
|
||||
}
|
||||
|
||||
function matchMenuItem(currentPath: string, menuItem: MenuItem): boolean {
|
||||
if ("path" in menuItem) {
|
||||
return !!matchPath(menuItem.path, currentPath);
|
||||
}
|
||||
|
||||
return menuItem.children.some((child) => matchMenuItem(currentPath, child));
|
||||
}
|
||||
|
||||
type NavLinkProps = {
|
||||
to: To;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
const NavLink: FunctionComponent<NavLinkProps> = ({
|
||||
to,
|
||||
isActive,
|
||||
children,
|
||||
}) => {
|
||||
const href = useHref(to);
|
||||
const handleClick = useLinkClickHandler(to);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
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}
|
||||
</NavItem>
|
||||
);
|
||||
};
|
14
apps/account-ui/src/root/Root.tsx
Normal file
14
apps/account-ui/src/root/Root.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Page, Spinner } from "@patternfly/react-core";
|
||||
import { Suspense } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { PageNav } from "./PageNav";
|
||||
|
||||
export const Root = () => (
|
||||
<Page header={<PageHeader />} sidebar={<PageNav />} isManagedSidebar>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Page>
|
||||
);
|
7
apps/account-ui/src/root/RootIndex.tsx
Normal file
7
apps/account-ui/src/root/RootIndex.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const RootIndex = () => {
|
||||
const { t } = useTranslation();
|
||||
return <PageSection>{t("welcomeMessage")}</PageSection>;
|
||||
};
|
72
apps/account-ui/src/routes.tsx
Normal file
72
apps/account-ui/src/routes.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { lazy } from "react";
|
||||
import type { IndexRouteObject, RouteObject } from "react-router";
|
||||
|
||||
import { ErrorPage } from "./root/ErrorPage";
|
||||
import { Root } from "./root/Root";
|
||||
import { RootIndex } from "./root/RootIndex";
|
||||
|
||||
const DeviceActivity = lazy(() => import("./account-security/DeviceActivity"));
|
||||
const LinkedAccounts = lazy(() => import("./account-security/LinkedAccounts"));
|
||||
const SigningIn = lazy(() => import("./account-security/SigningIn"));
|
||||
const Applications = lazy(() => import("./applications/Applications"));
|
||||
const Groups = lazy(() => import("./groups/Groups"));
|
||||
const PersonalInfo = lazy(() => import("./personal-info/PersonalInfo"));
|
||||
const Resources = lazy(() => import("./resources/Resources"));
|
||||
|
||||
export const DeviceActivityRoute: RouteObject = {
|
||||
path: "account-security/device-activity",
|
||||
element: <DeviceActivity />,
|
||||
};
|
||||
|
||||
export const LinkedAccountsRoute: RouteObject = {
|
||||
path: "account-security/linked-accounts",
|
||||
element: <LinkedAccounts />,
|
||||
};
|
||||
|
||||
export const SigningInRoute: RouteObject = {
|
||||
path: "account-security/signing-in",
|
||||
element: <SigningIn />,
|
||||
};
|
||||
|
||||
export const ApplicationsRoute: RouteObject = {
|
||||
path: "applications",
|
||||
element: <Applications />,
|
||||
};
|
||||
|
||||
export const GroupsRoute: RouteObject = {
|
||||
path: "groups",
|
||||
element: <Groups />,
|
||||
};
|
||||
|
||||
export const PersonalInfoRoute: RouteObject = {
|
||||
path: "personal-info",
|
||||
element: <PersonalInfo />,
|
||||
};
|
||||
|
||||
export const ResourcesRoute: RouteObject = {
|
||||
path: "resources",
|
||||
element: <Resources />,
|
||||
};
|
||||
|
||||
export const RootIndexRoute: IndexRouteObject = {
|
||||
index: true,
|
||||
element: <RootIndex />,
|
||||
};
|
||||
|
||||
export const RootRoute: RouteObject = {
|
||||
path: "/",
|
||||
element: <Root />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
RootIndexRoute,
|
||||
DeviceActivityRoute,
|
||||
LinkedAccountsRoute,
|
||||
SigningInRoute,
|
||||
ApplicationsRoute,
|
||||
GroupsRoute,
|
||||
PersonalInfoRoute,
|
||||
ResourcesRoute,
|
||||
],
|
||||
};
|
||||
|
||||
export const routes: RouteObject[] = [RootRoute];
|
1
apps/account-ui/src/vite-env.d.ts
vendored
Normal file
1
apps/account-ui/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
8
apps/account-ui/tsconfig.json
Normal file
8
apps/account-ui/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx"
|
||||
]
|
||||
}
|
17
apps/account-ui/vite.config.ts
Normal file
17
apps/account-ui/vite.config.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import checker from "vite-plugin-checker";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "",
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
resolve: {
|
||||
// Resolve the 'module' entrypoint at all times (not the default due to Node.js compatibility issues).
|
||||
mainFields: ["module"],
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
plugins: [react(), checker({ typescript: true })],
|
||||
});
|
7889
package-lock.json
generated
7889
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,7 @@
|
|||
"name": "keycloak-ui",
|
||||
"workspaces": [
|
||||
"libs/keycloak-js",
|
||||
"apps/account-ui",
|
||||
"apps/admin-ui"
|
||||
],
|
||||
"scripts": {
|
||||
|
|
Loading…
Reference in a new issue