account library (#26373)
* initial version Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * better init Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added more components Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * moved to public Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * use environment var to enter library mode Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * added export field Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
4b151e040f
commit
5d39625d82
28 changed files with 458 additions and 279 deletions
|
@ -1,6 +1,14 @@
|
|||
{
|
||||
"name": "account-ui",
|
||||
"type": "module",
|
||||
"main": "dist/account-ui.js",
|
||||
"types": "./dist/account-ui.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/account-ui.js",
|
||||
"types": "./dist/account-ui.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "wireit",
|
||||
"build": "wireit",
|
||||
|
|
|
@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { IconMapper, useAlerts } from "ui-shared";
|
||||
import { linkAccount, unLinkAccount } from "../api/methods";
|
||||
import { LinkedAccountRepresentation } from "../api/representations";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
|
||||
type AccountRowProps = {
|
||||
account: LinkedAccountRepresentation;
|
||||
|
@ -23,11 +24,12 @@ type AccountRowProps = {
|
|||
|
||||
export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const unLink = async (account: LinkedAccountRepresentation) => {
|
||||
try {
|
||||
await unLinkAccount(account);
|
||||
await unLinkAccount(context, account);
|
||||
addAlert(t("unLinkSuccess"));
|
||||
} catch (error) {
|
||||
addError(t("unLinkError", { error }).toString());
|
||||
|
@ -36,7 +38,7 @@ export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => {
|
|||
|
||||
const link = async (account: LinkedAccountRepresentation) => {
|
||||
try {
|
||||
const { accountLinkUri } = await linkAccount(account);
|
||||
const { accountLinkUri } = await linkAccount(context, account);
|
||||
location.href = accountLinkUri;
|
||||
} catch (error) {
|
||||
addError(t("linkError", { error }).toString());
|
||||
|
|
|
@ -32,12 +32,13 @@ import {
|
|||
} from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { TFuncKey } from "../i18n";
|
||||
import { keycloak } from "../keycloak";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { formatDate } from "../utils/formatDate";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
const DeviceActivity = () => {
|
||||
export const DeviceActivity = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [devices, setDevices] = useState<DeviceRepresentation[]>();
|
||||
|
@ -58,11 +59,13 @@ const DeviceActivity = () => {
|
|||
setDevices(devices);
|
||||
};
|
||||
|
||||
usePromise((signal) => getDevices({ signal }), moveCurrentToTop, [key]);
|
||||
usePromise((signal) => getDevices({ signal, context }), moveCurrentToTop, [
|
||||
key,
|
||||
]);
|
||||
|
||||
const signOutAll = async () => {
|
||||
await deleteSession();
|
||||
keycloak.logout();
|
||||
await deleteSession(context);
|
||||
context.keycloak.logout();
|
||||
};
|
||||
|
||||
const signOutSession = async (
|
||||
|
@ -70,7 +73,7 @@ const DeviceActivity = () => {
|
|||
device: DeviceRepresentation,
|
||||
) => {
|
||||
try {
|
||||
await deleteSession(session.id);
|
||||
await deleteSession(context, session.id);
|
||||
addAlert(
|
||||
t("signedOutSession", { browser: session.browser, os: device.os }),
|
||||
);
|
||||
|
|
|
@ -7,15 +7,19 @@ import { EmptyRow } from "../components/datalist/EmptyRow";
|
|||
import { Page } from "../components/page/Page";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { AccountRow } from "./AccountRow";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
|
||||
const LinkedAccounts = () => {
|
||||
export const LinkedAccounts = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const [accounts, setAccounts] = useState<LinkedAccountRepresentation[]>([]);
|
||||
|
||||
const [key, setKey] = useState(1);
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
||||
usePromise((signal) => getLinkedAccounts({ signal }), setAccounts, [key]);
|
||||
usePromise((signal) => getLinkedAccounts({ signal, context }), setAccounts, [
|
||||
key,
|
||||
]);
|
||||
|
||||
const linkedAccounts = useMemo(
|
||||
() => accounts.filter((account) => account.connected),
|
||||
|
|
|
@ -27,9 +27,9 @@ import {
|
|||
import { EmptyRow } from "../components/datalist/EmptyRow";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { TFuncKey } from "../i18n";
|
||||
import { keycloak } from "../keycloak";
|
||||
import { formatDate } from "../utils/formatDate";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
|
||||
type MobileLinkProps = {
|
||||
title: string;
|
||||
|
@ -63,16 +63,19 @@ const MobileLink = ({ title, onClick }: MobileLinkProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
const SigningIn = () => {
|
||||
export const SigningIn = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { login } = keycloak;
|
||||
const { login } = context.keycloak;
|
||||
|
||||
const [credentials, setCredentials] = useState<CredentialContainer[]>();
|
||||
const [key, setKey] = useState(1);
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
||||
usePromise((signal) => getCredentials({ signal }), setCredentials, [key]);
|
||||
usePromise((signal) => getCredentials({ signal, context }), setCredentials, [
|
||||
key,
|
||||
]);
|
||||
|
||||
const credentialRowCells = (
|
||||
credMetadata: CredentialMetadataRepresentation,
|
||||
|
@ -181,7 +184,10 @@ const SigningIn = () => {
|
|||
buttonVariant="danger"
|
||||
onContinue={async () => {
|
||||
try {
|
||||
await deleteCredentials(meta.credential);
|
||||
await deleteCredentials(
|
||||
context,
|
||||
meta.credential,
|
||||
);
|
||||
addAlert(
|
||||
t("successRemovedMessage", {
|
||||
userLabel: label(meta.credential),
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { CallOptions } from "./api/methods";
|
||||
import { Links, parseLinks } from "./api/parse-links";
|
||||
import { parseResponse } from "./api/parse-response";
|
||||
import { Permission, Resource, Scope } from "./api/representations";
|
||||
import { environment } from "./environment";
|
||||
import { keycloak } from "./keycloak";
|
||||
import { joinPath } from "./utils/joinPath";
|
||||
import { request } from "./api/request";
|
||||
import { KeycloakContext } from "./root/KeycloakContext";
|
||||
|
||||
export const fetchResources = async (
|
||||
params: RequestInit,
|
||||
{ signal, context }: CallOptions,
|
||||
requestParams: Record<string, string>,
|
||||
shared: boolean | undefined = false,
|
||||
): Promise<{ data: Resource[]; links: Links }> => {
|
||||
const response = await get(
|
||||
`/resources${shared ? "/shared-with-me?" : "?"}${
|
||||
shared ? "" : new URLSearchParams(requestParams)
|
||||
}`,
|
||||
params,
|
||||
const response = await request(
|
||||
`/resources${shared ? "/shared-with-me?" : "?"}`,
|
||||
context,
|
||||
{ searchParams: shared ? requestParams : undefined, signal },
|
||||
);
|
||||
|
||||
let links: Links;
|
||||
|
@ -31,77 +31,39 @@ export const fetchResources = async (
|
|||
};
|
||||
|
||||
export const fetchPermission = async (
|
||||
params: RequestInit,
|
||||
{ signal, context }: CallOptions,
|
||||
resourceId: string,
|
||||
): Promise<Permission[]> => {
|
||||
const response = await request<Permission[]>(
|
||||
const response = await request(
|
||||
`/resources/${resourceId}/permissions`,
|
||||
params,
|
||||
context,
|
||||
{ signal },
|
||||
);
|
||||
return checkResponse(response);
|
||||
return parseResponse<Permission[]>(response);
|
||||
};
|
||||
|
||||
export const updateRequest = (
|
||||
context: KeycloakContext,
|
||||
resourceId: string,
|
||||
username: string,
|
||||
scopes: Scope[] | string[],
|
||||
) =>
|
||||
request(`/resources/${resourceId}/permissions`, {
|
||||
method: "put",
|
||||
body: JSON.stringify([{ username, scopes }]),
|
||||
request(`/resources/${resourceId}/permissions`, context, {
|
||||
method: "PUT",
|
||||
body: [{ username, scopes }],
|
||||
});
|
||||
|
||||
export const updatePermissions = (
|
||||
context: KeycloakContext,
|
||||
resourceId: string,
|
||||
permissions: Permission[],
|
||||
) =>
|
||||
request(`/resources/${resourceId}/permissions`, {
|
||||
method: "put",
|
||||
body: JSON.stringify(permissions),
|
||||
request(`/resources/${resourceId}/permissions`, context, {
|
||||
method: "PUT",
|
||||
body: permissions,
|
||||
});
|
||||
|
||||
function checkResponse<T>(response: T) {
|
||||
if (!response) throw new Error("Could not fetch");
|
||||
return response;
|
||||
}
|
||||
|
||||
async function get(path: string, params: RequestInit): Promise<Response> {
|
||||
const url = joinPath(
|
||||
environment.authUrl,
|
||||
"realms",
|
||||
environment.realm,
|
||||
"account",
|
||||
path,
|
||||
);
|
||||
|
||||
const response = await fetch(url, {
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${await getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
params: RequestInit,
|
||||
): Promise<T | undefined> {
|
||||
const response = await get(path, params);
|
||||
if (response.status !== 204) return response.json();
|
||||
}
|
||||
|
||||
async function getAccessToken() {
|
||||
try {
|
||||
await keycloak.updateToken(5);
|
||||
} catch (error) {
|
||||
keycloak.login();
|
||||
}
|
||||
|
||||
return keycloak.token;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { environment } from "../environment";
|
||||
import { KeycloakContext } from "../root/KeycloakContext";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { parseResponse } from "./parse-response";
|
||||
import {
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
import { request } from "./request";
|
||||
|
||||
export type CallOptions = {
|
||||
context: KeycloakContext;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
|
@ -24,22 +25,27 @@ export type PaginationParams = {
|
|||
|
||||
export async function getPersonalInfo({
|
||||
signal,
|
||||
}: CallOptions = {}): Promise<UserRepresentation> {
|
||||
const response = await request("/?userProfileMetadata=true", { signal });
|
||||
context,
|
||||
}: CallOptions): Promise<UserRepresentation> {
|
||||
const response = await request("/?userProfileMetadata=true", context, {
|
||||
signal,
|
||||
});
|
||||
return parseResponse<UserRepresentation>(response);
|
||||
}
|
||||
|
||||
export async function getSupportedLocales({
|
||||
signal,
|
||||
}: CallOptions = {}): Promise<string[]> {
|
||||
const response = await request("/supportedLocales", { signal });
|
||||
context,
|
||||
}: CallOptions): Promise<string[]> {
|
||||
const response = await request("/supportedLocales", context, { signal });
|
||||
return parseResponse<string[]>(response);
|
||||
}
|
||||
|
||||
export async function savePersonalInfo(
|
||||
context: KeycloakContext,
|
||||
info: UserRepresentation,
|
||||
): Promise<void> {
|
||||
const response = await request("/", { body: info, method: "POST" });
|
||||
const response = await request("/", context, { body: info, method: "POST" });
|
||||
if (!response.ok) {
|
||||
const { errors } = await response.json();
|
||||
throw errors;
|
||||
|
@ -49,10 +55,11 @@ export async function savePersonalInfo(
|
|||
|
||||
export async function getPermissionRequests(
|
||||
resourceId: string,
|
||||
{ signal }: CallOptions = {},
|
||||
{ signal, context }: CallOptions,
|
||||
): Promise<Permission[]> {
|
||||
const response = await request(
|
||||
`/resources/${resourceId}/permissions/requests`,
|
||||
context,
|
||||
{ signal },
|
||||
);
|
||||
|
||||
|
@ -61,65 +68,89 @@ export async function getPermissionRequests(
|
|||
|
||||
export async function getDevices({
|
||||
signal,
|
||||
context,
|
||||
}: CallOptions): Promise<DeviceRepresentation[]> {
|
||||
const response = await request("/sessions/devices", { signal });
|
||||
const response = await request("/sessions/devices", context, { signal });
|
||||
return parseResponse<DeviceRepresentation[]>(response);
|
||||
}
|
||||
|
||||
export async function getApplications({ signal }: CallOptions = {}): Promise<
|
||||
ClientRepresentation[]
|
||||
> {
|
||||
const response = await request("/applications", { signal });
|
||||
export async function getApplications({
|
||||
signal,
|
||||
context,
|
||||
}: CallOptions): Promise<ClientRepresentation[]> {
|
||||
const response = await request("/applications", context, { signal });
|
||||
return parseResponse<ClientRepresentation[]>(response);
|
||||
}
|
||||
|
||||
export async function deleteConsent(id: string) {
|
||||
return request(`/applications/${id}/consent`, { method: "DELETE" });
|
||||
export async function deleteConsent(context: KeycloakContext, id: string) {
|
||||
return request(`/applications/${id}/consent`, context, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function deleteSession(id?: string) {
|
||||
return request(`/sessions${id ? `/${id}` : ""}`, {
|
||||
export async function deleteSession(context: KeycloakContext, id?: string) {
|
||||
return request(`/sessions${id ? `/${id}` : ""}`, context, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCredentials({ signal }: CallOptions) {
|
||||
const response = await request("/credentials", {
|
||||
export async function getCredentials({ signal, context }: CallOptions) {
|
||||
const response = await request("/credentials", context, {
|
||||
signal,
|
||||
});
|
||||
return parseResponse<CredentialContainer[]>(response);
|
||||
}
|
||||
|
||||
export async function deleteCredentials(credential: CredentialRepresentation) {
|
||||
return request("/credentials/" + credential.id, {
|
||||
export async function deleteCredentials(
|
||||
context: KeycloakContext,
|
||||
credential: CredentialRepresentation,
|
||||
) {
|
||||
return request("/credentials/" + credential.id, context, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLinkedAccounts({ signal }: CallOptions) {
|
||||
const response = await request("/linked-accounts", { signal });
|
||||
export async function getLinkedAccounts({ signal, context }: CallOptions) {
|
||||
const response = await request("/linked-accounts", context, { signal });
|
||||
return parseResponse<LinkedAccountRepresentation[]>(response);
|
||||
}
|
||||
|
||||
export async function unLinkAccount(account: LinkedAccountRepresentation) {
|
||||
const response = await request("/linked-accounts/" + account.providerName, {
|
||||
method: "DELETE",
|
||||
});
|
||||
export async function unLinkAccount(
|
||||
context: KeycloakContext,
|
||||
account: LinkedAccountRepresentation,
|
||||
) {
|
||||
const response = await request(
|
||||
"/linked-accounts/" + account.providerName,
|
||||
context,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
return parseResponse(response);
|
||||
}
|
||||
|
||||
export async function linkAccount(account: LinkedAccountRepresentation) {
|
||||
export async function linkAccount(
|
||||
context: KeycloakContext,
|
||||
account: LinkedAccountRepresentation,
|
||||
) {
|
||||
const redirectUri = encodeURIComponent(
|
||||
joinPath(environment.authUrl, "realms", environment.realm, "account"),
|
||||
joinPath(
|
||||
context.environment.authUrl,
|
||||
"realms",
|
||||
context.environment.realm,
|
||||
"account",
|
||||
),
|
||||
);
|
||||
const response = await request(
|
||||
"/linked-accounts/" + account.providerName,
|
||||
context,
|
||||
{
|
||||
searchParams: { providerId: account.providerName, redirectUri },
|
||||
},
|
||||
);
|
||||
const response = await request("/linked-accounts/" + account.providerName, {
|
||||
searchParams: { providerId: account.providerName, redirectUri },
|
||||
});
|
||||
return parseResponse<{ accountLinkUri: string }>(response);
|
||||
}
|
||||
|
||||
export async function getGroups({ signal }: CallOptions) {
|
||||
const response = await request("/groups", {
|
||||
export async function getGroups({ signal, context }: CallOptions) {
|
||||
const response = await request("/groups", context, {
|
||||
signal,
|
||||
});
|
||||
return parseResponse<Group[]>(response);
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
import { environment } from "../environment";
|
||||
import { keycloak } from "../keycloak";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { Environment } from "../environment";
|
||||
import Keycloak from "keycloak-js";
|
||||
import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { KeycloakContext } from "../root/KeycloakContext";
|
||||
|
||||
export type RequestOptions = {
|
||||
signal?: AbortSignal;
|
||||
getAccessToken?: () => Promise<string | undefined>;
|
||||
method?: "POST" | "PUT" | "DELETE";
|
||||
searchParams?: Record<string, string>;
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
export async function request(
|
||||
path: string,
|
||||
{ signal, method, searchParams, body }: RequestOptions = {},
|
||||
async function _request(
|
||||
url: URL,
|
||||
{ signal, getAccessToken, method, searchParams, body }: RequestOptions = {},
|
||||
): Promise<Response> {
|
||||
const url = new URL(
|
||||
joinPath(environment.authUrl, "realms", environment.realm, "account", path),
|
||||
);
|
||||
|
||||
if (searchParams) {
|
||||
Object.entries(searchParams).forEach(([key, value]) =>
|
||||
url.searchParams.set(key, value),
|
||||
|
@ -30,17 +28,34 @@ export async function request(
|
|||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON,
|
||||
authorization: `Bearer ${await getAccessToken()}`,
|
||||
authorization: `Bearer ${await getAccessToken?.()}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getAccessToken() {
|
||||
try {
|
||||
await keycloak.updateToken(5);
|
||||
} catch (error) {
|
||||
await keycloak.login();
|
||||
}
|
||||
|
||||
return keycloak.token;
|
||||
export async function request(
|
||||
path: string,
|
||||
{ environment, keycloak }: KeycloakContext,
|
||||
opts: RequestOptions = {},
|
||||
) {
|
||||
return _request(url(environment, path), {
|
||||
...opts,
|
||||
getAccessToken: token(keycloak),
|
||||
});
|
||||
}
|
||||
|
||||
export const url = (environment: Environment, path: string) =>
|
||||
new URL(
|
||||
joinPath(environment.authUrl, "realms", environment.realm, "account", path),
|
||||
);
|
||||
|
||||
export const token = (keycloak: Keycloak) =>
|
||||
async function getAccessToken() {
|
||||
try {
|
||||
await keycloak.updateToken(5);
|
||||
} catch (error) {
|
||||
await keycloak.login();
|
||||
}
|
||||
|
||||
return keycloak.token;
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import { deleteConsent, getApplications } from "../api/methods";
|
|||
import { ClientRepresentation } from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { TFuncKey } from "../i18n";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { formatDate } from "../utils/formatDate";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
|
@ -34,8 +35,9 @@ type Application = ClientRepresentation & {
|
|||
open: boolean;
|
||||
};
|
||||
|
||||
const Applications = () => {
|
||||
export const Applications = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [applications, setApplications] = useState<Application[]>();
|
||||
|
@ -43,7 +45,7 @@ const Applications = () => {
|
|||
const refresh = () => setKey(key + 1);
|
||||
|
||||
usePromise(
|
||||
(signal) => getApplications({ signal }),
|
||||
(signal) => getApplications({ signal, context }),
|
||||
(clients) => setApplications(clients.map((c) => ({ ...c, open: false }))),
|
||||
[key],
|
||||
);
|
||||
|
@ -58,7 +60,7 @@ const Applications = () => {
|
|||
|
||||
const removeConsent = async (id: string) => {
|
||||
try {
|
||||
await deleteConsent(id);
|
||||
await deleteConsent(context, id);
|
||||
refresh();
|
||||
addAlert(t("removeConsentSuccess"));
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Spinner } from "@patternfly/react-core";
|
||||
import { Suspense, lazy, useMemo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { environment } from "../environment";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { MenuItem } from "../root/PageNav";
|
||||
import { ContentComponentParams } from "../routes";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import fetchContentJson from "./fetchContent";
|
||||
import { MenuItem } from "../root/PageNav";
|
||||
|
||||
function findComponent(
|
||||
content: MenuItem[],
|
||||
|
@ -27,11 +27,13 @@ function findComponent(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const ContentComponent = () => {
|
||||
export const ContentComponent = () => {
|
||||
const context = useEnvironment();
|
||||
|
||||
const [content, setContent] = useState<MenuItem[]>();
|
||||
const { componentId } = useParams<ContentComponentParams>();
|
||||
|
||||
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
||||
usePromise((signal) => fetchContentJson({ signal, context }), setContent);
|
||||
const modulePath = useMemo(
|
||||
() => findComponent(content || [], componentId!),
|
||||
[content, componentId],
|
||||
|
@ -45,6 +47,8 @@ type ComponentProps = {
|
|||
};
|
||||
|
||||
const Component = ({ modulePath }: ComponentProps) => {
|
||||
const { environment } = useEnvironment();
|
||||
|
||||
const Element = lazy(
|
||||
() => import(joinPath(environment.resourceUrl, modulePath)),
|
||||
);
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { CallOptions } from "../api/methods";
|
||||
import { environment } from "../environment";
|
||||
import { MenuItem } from "../root/PageNav";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
|
||||
export default async function fetchContentJson(
|
||||
opts: CallOptions = {},
|
||||
opts: CallOptions,
|
||||
): Promise<MenuItem[]> {
|
||||
const response = await fetch(
|
||||
joinPath(environment.resourceUrl, "/content.json"),
|
||||
joinPath(opts.context.environment.resourceUrl, "/content.json"),
|
||||
opts,
|
||||
);
|
||||
return await response.json();
|
||||
|
|
|
@ -11,16 +11,18 @@ import { useTranslation } from "react-i18next";
|
|||
import { getGroups } from "../api/methods";
|
||||
import { Group } from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
const Groups = () => {
|
||||
export const Groups = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [directMembership, setDirectMembership] = useState(false);
|
||||
|
||||
usePromise(
|
||||
(signal) => getGroups({ signal }),
|
||||
(signal) => getGroups({ signal, context }),
|
||||
(groups) => {
|
||||
if (directMembership) {
|
||||
groups.forEach((el) =>
|
||||
|
|
42
js/apps/account-ui/src/index.ts
Normal file
42
js/apps/account-ui/src/index.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
export { PersonalInfo } from "./personal-info/PersonalInfo";
|
||||
export { ErrorPage } from "./root/ErrorPage";
|
||||
export { Header } from "./root/Header";
|
||||
export { KeycloakProvider, useEnvironment } from "./root/KeycloakContext";
|
||||
export { PageNav } from "./root/PageNav";
|
||||
export { DeviceActivity } from "./account-security/DeviceActivity";
|
||||
export { LinkedAccounts } from "./account-security/LinkedAccounts";
|
||||
export { SigningIn } from "./account-security/SigningIn";
|
||||
export type {
|
||||
AccountLinkUriRepresentation,
|
||||
Client,
|
||||
ClientRepresentation,
|
||||
ConsentRepresentation,
|
||||
ConsentScopeRepresentation,
|
||||
CredentialContainer,
|
||||
CredentialMetadataRepresentation,
|
||||
CredentialRepresentation,
|
||||
CredentialTypeMetadata,
|
||||
DeviceRepresentation,
|
||||
Group,
|
||||
LinkedAccountRepresentation,
|
||||
Permission,
|
||||
Permissions,
|
||||
Resource,
|
||||
Scope,
|
||||
SessionRepresentation,
|
||||
UserProfileAttributeMetadata,
|
||||
UserProfileMetadata,
|
||||
UserRepresentation,
|
||||
} from "./api/representations";
|
||||
export { Applications } from "./applications/Applications";
|
||||
export { EmptyRow } from "./components/datalist/EmptyRow";
|
||||
export { Page } from "./components/page/Page";
|
||||
export { ContentComponent } from "./content/ContentComponent";
|
||||
export { Groups } from "./groups/Groups";
|
||||
export { EditTheResource } from "./resources/EditTheResource";
|
||||
export { PermissionRequest } from "./resources/PermissionRequest";
|
||||
export { Resources } from "./resources/Resources";
|
||||
export { ResourcesTab } from "./resources/ResourcesTab";
|
||||
export { ResourceToolbar } from "./resources/ResourceToolbar";
|
||||
export { SharedWith } from "./resources/SharedWith";
|
||||
export { ShareTheResource } from "./resources/ShareTheResource";
|
|
@ -1,8 +0,0 @@
|
|||
import Keycloak from "keycloak-js";
|
||||
import { environment } from "./environment";
|
||||
|
||||
export const keycloak = new Keycloak({
|
||||
url: environment.authUrl,
|
||||
realm: environment.realm,
|
||||
clientId: environment.clientId,
|
||||
});
|
|
@ -6,16 +6,10 @@ import { createRoot } from "react-dom/client";
|
|||
import { createHashRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
import { i18n } from "./i18n";
|
||||
import { keycloak } from "./keycloak";
|
||||
import { routes } from "./routes";
|
||||
|
||||
// Initialize required components before rendering app.
|
||||
await Promise.all([
|
||||
keycloak.init({
|
||||
onLoad: "check-sso",
|
||||
}),
|
||||
i18n.init(),
|
||||
]);
|
||||
await i18n.init();
|
||||
|
||||
const router = createHashRouter(routes);
|
||||
const container = document.getElementById("app");
|
||||
|
|
|
@ -28,12 +28,13 @@ import {
|
|||
UserRepresentation,
|
||||
} from "../api/representations";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { environment } from "../environment";
|
||||
import { TFuncKey, i18n } from "../i18n";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
const PersonalInfo = () => {
|
||||
export const PersonalInfo = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const keycloak = useKeycloak();
|
||||
const [userProfileMetadata, setUserProfileMetadata] =
|
||||
useState<UserProfileMetadata>();
|
||||
|
@ -45,8 +46,8 @@ const PersonalInfo = () => {
|
|||
usePromise(
|
||||
(signal) =>
|
||||
Promise.all([
|
||||
getPersonalInfo({ signal }),
|
||||
getSupportedLocales({ signal }),
|
||||
getPersonalInfo({ signal, context }),
|
||||
getSupportedLocales({ signal, context }),
|
||||
]),
|
||||
([personalInfo, supportedLocales]) => {
|
||||
setUserProfileMetadata(personalInfo.userProfileMetadata);
|
||||
|
@ -57,7 +58,7 @@ const PersonalInfo = () => {
|
|||
|
||||
const onSubmit = async (user: UserRepresentation) => {
|
||||
try {
|
||||
await savePersonalInfo(user);
|
||||
await savePersonalInfo(context, user);
|
||||
const locale = user.attributes?.["locale"]?.toString();
|
||||
i18n.changeLanguage(locale, (error) => {
|
||||
if (error) {
|
||||
|
@ -87,7 +88,7 @@ const PersonalInfo = () => {
|
|||
updateEmailActionEnabled,
|
||||
isRegistrationEmailAsUsername,
|
||||
isEditUserNameAllowed,
|
||||
} = environment.features;
|
||||
} = context.environment.features;
|
||||
return (
|
||||
<Page title={t("personalInfo")} description={t("personalInfoDescription")}>
|
||||
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
|
||||
|
@ -136,7 +137,7 @@ const PersonalInfo = () => {
|
|||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
{environment.features.deleteAccountAllowed && (
|
||||
{context.environment.features.deleteAccountAllowed && (
|
||||
<ExpandableSection
|
||||
data-testid="delete-account"
|
||||
toggleText={t("deleteAccount")}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { KeycloakTextInput, SelectControl, useAlerts } from "ui-shared";
|
||||
import { updatePermissions } from "../api";
|
||||
import type { Permission, Resource } from "../api/representations";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
|
||||
type EditTheResourceProps = {
|
||||
resource: Resource;
|
||||
|
@ -23,6 +24,7 @@ export const EditTheResource = ({
|
|||
onClose,
|
||||
}: EditTheResourceProps) => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const form = useForm<FormValues>();
|
||||
|
@ -39,7 +41,7 @@ export const EditTheResource = ({
|
|||
try {
|
||||
await Promise.all(
|
||||
permissions.map((permission) =>
|
||||
updatePermissions(resource._id, [permission]),
|
||||
updatePermissions(context, resource._id, [permission]),
|
||||
),
|
||||
);
|
||||
addAlert(t("updateSuccess"));
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { useAlerts } from "ui-shared";
|
||||
import { fetchPermission, updateRequest } from "../api";
|
||||
import { Permission, Resource } from "../api/representations";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
|
||||
type PermissionRequestProps = {
|
||||
resource: Resource;
|
||||
|
@ -32,6 +33,7 @@ export const PermissionRequest = ({
|
|||
refresh,
|
||||
}: PermissionRequestProps) => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
@ -43,12 +45,13 @@ export const PermissionRequest = ({
|
|||
approve: boolean = false,
|
||||
) => {
|
||||
try {
|
||||
const permissions = await fetchPermission({}, resource._id);
|
||||
const permissions = await fetchPermission({ context }, resource._id);
|
||||
const { scopes, username } = permissions.find(
|
||||
(p) => p.username === shareRequest.username,
|
||||
)!;
|
||||
|
||||
await updateRequest(
|
||||
context,
|
||||
resource._id,
|
||||
username,
|
||||
approve
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
|
|||
import { ResourcesTab } from "./ResourcesTab";
|
||||
import { Page } from "../components/page/Page";
|
||||
|
||||
const Resources = () => {
|
||||
export const Resources = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTabKey, setActiveTabKey] = useState(0);
|
||||
|
||||
|
|
|
@ -31,17 +31,18 @@ import {
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ContinueCancelModal, useAlerts } from "ui-shared";
|
||||
import { fetchPermission, fetchResources, updatePermissions } from "../api";
|
||||
import { getPermissionRequests } from "../api/methods";
|
||||
import { Links } from "../api/parse-links";
|
||||
import { Permission, Resource } from "../api/representations";
|
||||
import { ContinueCancelModal, useAlerts } from "ui-shared";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { EditTheResource } from "./EditTheResource";
|
||||
import { PermissionRequest } from "./PermissionRequest";
|
||||
import { ResourceToolbar } from "./ResourceToolbar";
|
||||
import { SharedWith } from "./SharedWith";
|
||||
import { ShareTheResource } from "./ShareTheResource";
|
||||
import { SharedWith } from "./SharedWith";
|
||||
|
||||
type PermissionDetail = {
|
||||
contextOpen?: boolean;
|
||||
|
@ -57,6 +58,7 @@ type ResourcesTabProps = {
|
|||
|
||||
export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [params, setParams] = useState<Record<string, string>>({
|
||||
|
@ -73,13 +75,18 @@ export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
|
|||
|
||||
usePromise(
|
||||
async (signal) => {
|
||||
const result = await fetchResources({ signal }, params, isShared);
|
||||
const result = await fetchResources(
|
||||
{ signal, context },
|
||||
params,
|
||||
isShared,
|
||||
);
|
||||
if (!isShared)
|
||||
await Promise.all(
|
||||
result.data.map(
|
||||
async (r) =>
|
||||
(r.shareRequests = await getPermissionRequests(r._id, {
|
||||
signal,
|
||||
context,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
@ -99,7 +106,7 @@ export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
|
|||
const fetchPermissions = async (id: string) => {
|
||||
let permissions = details[id]?.permissions || [];
|
||||
if (!details[id]) {
|
||||
permissions = await fetchPermission({}, id);
|
||||
permissions = await fetchPermission({ context }, id);
|
||||
}
|
||||
return permissions;
|
||||
};
|
||||
|
@ -113,7 +120,7 @@ export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
|
|||
scopes: [],
|
||||
}) as Permission,
|
||||
)!;
|
||||
await updatePermissions(resource._id, permissions);
|
||||
await updatePermissions(context, resource._id, permissions);
|
||||
setDetails({});
|
||||
addAlert(t("unShareSuccess"));
|
||||
} catch (error) {
|
||||
|
|
|
@ -17,9 +17,10 @@ import {
|
|||
} from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { KeycloakTextInput, SelectControl, useAlerts } from "ui-shared";
|
||||
import { updateRequest } from "../api";
|
||||
import { Permission, Resource } from "../api/representations";
|
||||
import { useAlerts, SelectControl, KeycloakTextInput } from "ui-shared";
|
||||
import { useEnvironment } from "../root/KeycloakContext";
|
||||
import { SharedWith } from "./SharedWith";
|
||||
|
||||
type ShareTheResourceProps = {
|
||||
|
@ -41,6 +42,7 @@ export const ShareTheResource = ({
|
|||
onClose,
|
||||
}: ShareTheResourceProps) => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const form = useForm<FormValues>();
|
||||
const {
|
||||
|
@ -79,7 +81,7 @@ export const ShareTheResource = ({
|
|||
usernames
|
||||
.filter(({ value }) => value !== "")
|
||||
.map(({ value: username }) =>
|
||||
updateRequest(resource._id, username, permissions),
|
||||
updateRequest(context, resource._id, username, permissions),
|
||||
),
|
||||
);
|
||||
addAlert(t("shareSuccess"));
|
||||
|
|
74
js/apps/account-ui/src/root/Header.tsx
Normal file
74
js/apps/account-ui/src/root/Header.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
KeycloakMasthead,
|
||||
KeycloakProvider,
|
||||
Translations,
|
||||
TranslationsProvider,
|
||||
} from "keycloak-masthead";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHref } from "react-router-dom";
|
||||
import { useEnvironment } from "./KeycloakContext";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
|
||||
import { Button } from "@patternfly/react-core";
|
||||
|
||||
import style from "./header.module.css";
|
||||
|
||||
const ReferrerLink = () => {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
|
||||
return searchParams.has("referrer_uri") ? (
|
||||
<Button
|
||||
data-testid="referrer-link"
|
||||
component="a"
|
||||
href={searchParams.get("referrer_uri")!.replace("_hash_", "#")}
|
||||
variant="link"
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
>
|
||||
{t("backTo", { app: searchParams.get("referrer") })}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const { environment, keycloak } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const brandImage = environment.logo || "logo.svg";
|
||||
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;
|
||||
const translations = useMemo<Translations>(
|
||||
() => ({
|
||||
avatar: t("avatar"),
|
||||
fullName: t("fullName"),
|
||||
manageAccount: t("manageAccount"),
|
||||
signOut: t("signOut"),
|
||||
unknownUser: t("unknownUser"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<TranslationsProvider translations={translations}>
|
||||
<KeycloakProvider keycloak={keycloak}>
|
||||
<KeycloakMasthead
|
||||
features={{ hasManageAccount: false }}
|
||||
showNavToggle
|
||||
brand={{
|
||||
href: indexHref,
|
||||
src: joinPath(environment.resourceUrl, brandImage),
|
||||
alt: t("logo"),
|
||||
className: style.brand,
|
||||
}}
|
||||
toolbarItems={[<ReferrerLink key="link" />]}
|
||||
/>
|
||||
</KeycloakProvider>
|
||||
</TranslationsProvider>
|
||||
);
|
||||
};
|
77
js/apps/account-ui/src/root/KeycloakContext.tsx
Normal file
77
js/apps/account-ui/src/root/KeycloakContext.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { Spinner } from "@patternfly/react-core";
|
||||
import Keycloak from "keycloak-js";
|
||||
import {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { AlertProvider, Help } from "ui-shared";
|
||||
import { Environment } from "../environment";
|
||||
|
||||
export type KeycloakContext = KeycloakContextProps & {
|
||||
keycloak: Keycloak;
|
||||
};
|
||||
|
||||
const KeycloakEnvContext = createContext<KeycloakContext | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const useEnvironment = () => {
|
||||
const context = useContext(KeycloakEnvContext);
|
||||
if (!context)
|
||||
throw Error(
|
||||
"no environment provider in the hierarchy make sure to add the provider",
|
||||
);
|
||||
return context;
|
||||
};
|
||||
|
||||
type KeycloakContextProps = {
|
||||
environment: Environment;
|
||||
};
|
||||
|
||||
export const KeycloakProvider = ({
|
||||
environment,
|
||||
children,
|
||||
}: PropsWithChildren<KeycloakContextProps>) => {
|
||||
const calledOnce = useRef(false);
|
||||
const [init, setInit] = useState(false);
|
||||
const keycloak = useMemo(
|
||||
() =>
|
||||
new Keycloak({
|
||||
url: environment.authUrl,
|
||||
realm: environment.realm,
|
||||
clientId: environment.clientId,
|
||||
}),
|
||||
[environment],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// only needed in dev mode
|
||||
if (calledOnce.current) {
|
||||
return;
|
||||
}
|
||||
const init = () => {
|
||||
return keycloak.init({
|
||||
onLoad: "check-sso",
|
||||
pkceMethod: "S256",
|
||||
responseMode: "query",
|
||||
});
|
||||
};
|
||||
init().then(() => setInit(true));
|
||||
calledOnce.current = true;
|
||||
}, [keycloak]);
|
||||
|
||||
if (!init) return <Spinner />;
|
||||
|
||||
return (
|
||||
<KeycloakEnvContext.Provider value={{ environment, keycloak }}>
|
||||
<AlertProvider>
|
||||
<Help>{children}</Help>
|
||||
</AlertProvider>
|
||||
</KeycloakEnvContext.Provider>
|
||||
);
|
||||
};
|
|
@ -22,29 +22,31 @@ import {
|
|||
useLocation,
|
||||
} from "react-router-dom";
|
||||
import fetchContentJson from "../content/fetchContent";
|
||||
import type { Feature } from "../environment";
|
||||
import { TFuncKey } from "../i18n";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
import { Feature, environment } from "../environment";
|
||||
import { useEnvironment } from "./KeycloakContext";
|
||||
|
||||
type RootMenuItem = {
|
||||
label: TFuncKey;
|
||||
path: string;
|
||||
isHidden?: keyof Feature;
|
||||
isVisible?: keyof Feature;
|
||||
modulePath?: string;
|
||||
};
|
||||
|
||||
type MenuItemWithChildren = {
|
||||
label: TFuncKey;
|
||||
children: MenuItem[];
|
||||
isHidden?: keyof Feature;
|
||||
isVisible?: keyof Feature;
|
||||
};
|
||||
|
||||
export type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||
|
||||
export const PageNav = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>();
|
||||
const context = useEnvironment();
|
||||
|
||||
usePromise((signal) => fetchContentJson({ signal }), setMenuItems);
|
||||
usePromise((signal) => fetchContentJson({ signal, context }), setMenuItems);
|
||||
return (
|
||||
<PageSidebar
|
||||
nav={
|
||||
|
@ -53,8 +55,8 @@ export const PageNav = () => {
|
|||
<Suspense fallback={<Spinner />}>
|
||||
{menuItems
|
||||
?.filter((menuItem) =>
|
||||
menuItem.isHidden
|
||||
? environment.features[menuItem.isHidden]
|
||||
menuItem.isVisible
|
||||
? context.environment.features[menuItem.isVisible]
|
||||
: true,
|
||||
)
|
||||
.map((menuItem) => (
|
||||
|
@ -77,6 +79,9 @@ type NavMenuItemProps = {
|
|||
|
||||
function NavMenuItem({ menuItem }: NavMenuItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
environment: { features },
|
||||
} = useEnvironment();
|
||||
const { pathname } = useLocation();
|
||||
const isActive = useMemo(
|
||||
() => matchMenuItem(pathname, menuItem),
|
||||
|
@ -99,7 +104,9 @@ function NavMenuItem({ menuItem }: NavMenuItemProps) {
|
|||
isExpanded={isActive}
|
||||
>
|
||||
{menuItem.children
|
||||
.filter((menuItem) => !menuItem.isHidden)
|
||||
.filter((menuItem) =>
|
||||
menuItem.isVisible ? features[menuItem.isVisible] : true,
|
||||
)
|
||||
.map((child) => (
|
||||
<NavMenuItem key={child.label as string} menuItem={child} />
|
||||
))}
|
||||
|
|
|
@ -1,89 +1,18 @@
|
|||
import { Button, Page, Spinner } from "@patternfly/react-core";
|
||||
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
|
||||
import {
|
||||
KeycloakMasthead,
|
||||
KeycloakProvider,
|
||||
Translations,
|
||||
TranslationsProvider,
|
||||
} from "keycloak-masthead";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Outlet, useHref } from "react-router-dom";
|
||||
import { AlertProvider, Help } from "ui-shared";
|
||||
import { Page, Spinner } from "@patternfly/react-core";
|
||||
import { Suspense } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { environment } from "../environment";
|
||||
import { keycloak } from "../keycloak";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { Header } from "./Header";
|
||||
import { KeycloakProvider } from "./KeycloakContext";
|
||||
import { PageNav } from "./PageNav";
|
||||
|
||||
import style from "./Root.module.css";
|
||||
|
||||
const ReferrerLink = () => {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
|
||||
return searchParams.has("referrer_uri") ? (
|
||||
<Button
|
||||
data-testid="referrer-link"
|
||||
component="a"
|
||||
href={searchParams.get("referrer_uri")!.replace("_hash_", "#")}
|
||||
variant="link"
|
||||
icon={<ExternalLinkSquareAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
>
|
||||
{t("backTo", { app: searchParams.get("referrer") })}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const Root = () => {
|
||||
const { t } = useTranslation();
|
||||
const brandImage = environment.logo || "logo.svg";
|
||||
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;
|
||||
|
||||
const translations = useMemo<Translations>(
|
||||
() => ({
|
||||
avatar: t("avatar"),
|
||||
fullName: t("fullName"),
|
||||
manageAccount: t("manageAccount"),
|
||||
signOut: t("signOut"),
|
||||
unknownUser: t("unknownUser"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<KeycloakProvider keycloak={keycloak}>
|
||||
<Page
|
||||
header={
|
||||
<TranslationsProvider translations={translations}>
|
||||
<KeycloakMasthead
|
||||
features={{ hasManageAccount: false }}
|
||||
showNavToggle
|
||||
brand={{
|
||||
href: indexHref,
|
||||
src: joinPath(environment.resourceUrl, brandImage),
|
||||
alt: t("logo"),
|
||||
className: style.brand,
|
||||
}}
|
||||
toolbarItems={[<ReferrerLink key="link" />]}
|
||||
/>
|
||||
</TranslationsProvider>
|
||||
}
|
||||
sidebar={<PageNav />}
|
||||
isManagedSidebar
|
||||
>
|
||||
<AlertProvider>
|
||||
<Help>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Help>
|
||||
</AlertProvider>
|
||||
<KeycloakProvider environment={environment}>
|
||||
<Page header={<Header />} sidebar={<PageNav />} isManagedSidebar>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Page>
|
||||
</KeycloakProvider>
|
||||
);
|
||||
|
|
|
@ -3,11 +3,7 @@ import { login } from "./login";
|
|||
|
||||
test("Check page heading", async ({ page }) => {
|
||||
await login(page, "alice", "alice", "user-profile");
|
||||
await page
|
||||
.getByRole("button", {
|
||||
name: "Account security",
|
||||
})
|
||||
.click();
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
|
||||
const linkedAccountsNavItem = page.getByTestId(
|
||||
"account-security/linked-accounts",
|
||||
|
|
|
@ -1,21 +1,36 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import { checker } from "vite-plugin-checker";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "",
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
target: "esnext",
|
||||
modulePreload: false,
|
||||
cssMinify: "lightningcss",
|
||||
rollupOptions: {
|
||||
external: ["react", "react/jsx-runtime", "react-dom"],
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
const external = ["react", "react/jsx-runtime", "react-dom"];
|
||||
if (env.LIB) external.push("react-router-dom");
|
||||
const lib = env.LIB
|
||||
? {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "src/index.ts"),
|
||||
formats: ["es"],
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
return {
|
||||
base: "",
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
},
|
||||
plugins: [react(), checker({ typescript: true })],
|
||||
build: {
|
||||
...lib,
|
||||
sourcemap: true,
|
||||
target: "esnext",
|
||||
modulePreload: false,
|
||||
cssMinify: "lightningcss",
|
||||
rollupOptions: {
|
||||
external: external,
|
||||
},
|
||||
},
|
||||
plugins: [react(), checker({ typescript: true })],
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue