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:
Erik Jan de Wit 2024-02-01 16:46:11 +01:00 committed by GitHub
parent 4b151e040f
commit 5d39625d82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 458 additions and 279 deletions

View file

@ -1,6 +1,14 @@
{ {
"name": "account-ui", "name": "account-ui",
"type": "module", "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": { "scripts": {
"dev": "wireit", "dev": "wireit",
"build": "wireit", "build": "wireit",

View file

@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
import { IconMapper, useAlerts } from "ui-shared"; import { IconMapper, useAlerts } from "ui-shared";
import { linkAccount, unLinkAccount } from "../api/methods"; import { linkAccount, unLinkAccount } from "../api/methods";
import { LinkedAccountRepresentation } from "../api/representations"; import { LinkedAccountRepresentation } from "../api/representations";
import { useEnvironment } from "../root/KeycloakContext";
type AccountRowProps = { type AccountRowProps = {
account: LinkedAccountRepresentation; account: LinkedAccountRepresentation;
@ -23,11 +24,12 @@ type AccountRowProps = {
export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => { export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const unLink = async (account: LinkedAccountRepresentation) => { const unLink = async (account: LinkedAccountRepresentation) => {
try { try {
await unLinkAccount(account); await unLinkAccount(context, account);
addAlert(t("unLinkSuccess")); addAlert(t("unLinkSuccess"));
} catch (error) { } catch (error) {
addError(t("unLinkError", { error }).toString()); addError(t("unLinkError", { error }).toString());
@ -36,7 +38,7 @@ export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => {
const link = async (account: LinkedAccountRepresentation) => { const link = async (account: LinkedAccountRepresentation) => {
try { try {
const { accountLinkUri } = await linkAccount(account); const { accountLinkUri } = await linkAccount(context, account);
location.href = accountLinkUri; location.href = accountLinkUri;
} catch (error) { } catch (error) {
addError(t("linkError", { error }).toString()); addError(t("linkError", { error }).toString());

View file

@ -32,12 +32,13 @@ import {
} from "../api/representations"; } from "../api/representations";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import { TFuncKey } from "../i18n"; import { TFuncKey } from "../i18n";
import { keycloak } from "../keycloak"; import { useEnvironment } from "../root/KeycloakContext";
import { formatDate } from "../utils/formatDate"; import { formatDate } from "../utils/formatDate";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
const DeviceActivity = () => { export const DeviceActivity = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [devices, setDevices] = useState<DeviceRepresentation[]>(); const [devices, setDevices] = useState<DeviceRepresentation[]>();
@ -58,11 +59,13 @@ const DeviceActivity = () => {
setDevices(devices); setDevices(devices);
}; };
usePromise((signal) => getDevices({ signal }), moveCurrentToTop, [key]); usePromise((signal) => getDevices({ signal, context }), moveCurrentToTop, [
key,
]);
const signOutAll = async () => { const signOutAll = async () => {
await deleteSession(); await deleteSession(context);
keycloak.logout(); context.keycloak.logout();
}; };
const signOutSession = async ( const signOutSession = async (
@ -70,7 +73,7 @@ const DeviceActivity = () => {
device: DeviceRepresentation, device: DeviceRepresentation,
) => { ) => {
try { try {
await deleteSession(session.id); await deleteSession(context, session.id);
addAlert( addAlert(
t("signedOutSession", { browser: session.browser, os: device.os }), t("signedOutSession", { browser: session.browser, os: device.os }),
); );

View file

@ -7,15 +7,19 @@ import { EmptyRow } from "../components/datalist/EmptyRow";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
import { AccountRow } from "./AccountRow"; import { AccountRow } from "./AccountRow";
import { useEnvironment } from "../root/KeycloakContext";
const LinkedAccounts = () => { export const LinkedAccounts = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const [accounts, setAccounts] = useState<LinkedAccountRepresentation[]>([]); const [accounts, setAccounts] = useState<LinkedAccountRepresentation[]>([]);
const [key, setKey] = useState(1); const [key, setKey] = useState(1);
const refresh = () => setKey(key + 1); const refresh = () => setKey(key + 1);
usePromise((signal) => getLinkedAccounts({ signal }), setAccounts, [key]); usePromise((signal) => getLinkedAccounts({ signal, context }), setAccounts, [
key,
]);
const linkedAccounts = useMemo( const linkedAccounts = useMemo(
() => accounts.filter((account) => account.connected), () => accounts.filter((account) => account.connected),

View file

@ -27,9 +27,9 @@ import {
import { EmptyRow } from "../components/datalist/EmptyRow"; import { EmptyRow } from "../components/datalist/EmptyRow";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import { TFuncKey } from "../i18n"; import { TFuncKey } from "../i18n";
import { keycloak } from "../keycloak";
import { formatDate } from "../utils/formatDate"; import { formatDate } from "../utils/formatDate";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
import { useEnvironment } from "../root/KeycloakContext";
type MobileLinkProps = { type MobileLinkProps = {
title: string; title: string;
@ -63,16 +63,19 @@ const MobileLink = ({ title, onClick }: MobileLinkProps) => {
); );
}; };
const SigningIn = () => { export const SigningIn = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { login } = keycloak; const { login } = context.keycloak;
const [credentials, setCredentials] = useState<CredentialContainer[]>(); const [credentials, setCredentials] = useState<CredentialContainer[]>();
const [key, setKey] = useState(1); const [key, setKey] = useState(1);
const refresh = () => setKey(key + 1); const refresh = () => setKey(key + 1);
usePromise((signal) => getCredentials({ signal }), setCredentials, [key]); usePromise((signal) => getCredentials({ signal, context }), setCredentials, [
key,
]);
const credentialRowCells = ( const credentialRowCells = (
credMetadata: CredentialMetadataRepresentation, credMetadata: CredentialMetadataRepresentation,
@ -181,7 +184,10 @@ const SigningIn = () => {
buttonVariant="danger" buttonVariant="danger"
onContinue={async () => { onContinue={async () => {
try { try {
await deleteCredentials(meta.credential); await deleteCredentials(
context,
meta.credential,
);
addAlert( addAlert(
t("successRemovedMessage", { t("successRemovedMessage", {
userLabel: label(meta.credential), userLabel: label(meta.credential),

View file

@ -1,19 +1,19 @@
import { CallOptions } from "./api/methods";
import { Links, parseLinks } from "./api/parse-links"; import { Links, parseLinks } from "./api/parse-links";
import { parseResponse } from "./api/parse-response";
import { Permission, Resource, Scope } from "./api/representations"; import { Permission, Resource, Scope } from "./api/representations";
import { environment } from "./environment"; import { request } from "./api/request";
import { keycloak } from "./keycloak"; import { KeycloakContext } from "./root/KeycloakContext";
import { joinPath } from "./utils/joinPath";
export const fetchResources = async ( export const fetchResources = async (
params: RequestInit, { signal, context }: CallOptions,
requestParams: Record<string, string>, requestParams: Record<string, string>,
shared: boolean | undefined = false, shared: boolean | undefined = false,
): Promise<{ data: Resource[]; links: Links }> => { ): Promise<{ data: Resource[]; links: Links }> => {
const response = await get( const response = await request(
`/resources${shared ? "/shared-with-me?" : "?"}${ `/resources${shared ? "/shared-with-me?" : "?"}`,
shared ? "" : new URLSearchParams(requestParams) context,
}`, { searchParams: shared ? requestParams : undefined, signal },
params,
); );
let links: Links; let links: Links;
@ -31,77 +31,39 @@ export const fetchResources = async (
}; };
export const fetchPermission = async ( export const fetchPermission = async (
params: RequestInit, { signal, context }: CallOptions,
resourceId: string, resourceId: string,
): Promise<Permission[]> => { ): Promise<Permission[]> => {
const response = await request<Permission[]>( const response = await request(
`/resources/${resourceId}/permissions`, `/resources/${resourceId}/permissions`,
params, context,
{ signal },
); );
return checkResponse(response); return parseResponse<Permission[]>(response);
}; };
export const updateRequest = ( export const updateRequest = (
context: KeycloakContext,
resourceId: string, resourceId: string,
username: string, username: string,
scopes: Scope[] | string[], scopes: Scope[] | string[],
) => ) =>
request(`/resources/${resourceId}/permissions`, { request(`/resources/${resourceId}/permissions`, context, {
method: "put", method: "PUT",
body: JSON.stringify([{ username, scopes }]), body: [{ username, scopes }],
}); });
export const updatePermissions = ( export const updatePermissions = (
context: KeycloakContext,
resourceId: string, resourceId: string,
permissions: Permission[], permissions: Permission[],
) => ) =>
request(`/resources/${resourceId}/permissions`, { request(`/resources/${resourceId}/permissions`, context, {
method: "put", method: "PUT",
body: JSON.stringify(permissions), body: permissions,
}); });
function checkResponse<T>(response: T) { function checkResponse<T>(response: T) {
if (!response) throw new Error("Could not fetch"); if (!response) throw new Error("Could not fetch");
return response; 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;
}

View file

@ -1,4 +1,4 @@
import { environment } from "../environment"; import { KeycloakContext } from "../root/KeycloakContext";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
import { parseResponse } from "./parse-response"; import { parseResponse } from "./parse-response";
import { import {
@ -14,6 +14,7 @@ import {
import { request } from "./request"; import { request } from "./request";
export type CallOptions = { export type CallOptions = {
context: KeycloakContext;
signal?: AbortSignal; signal?: AbortSignal;
}; };
@ -24,22 +25,27 @@ export type PaginationParams = {
export async function getPersonalInfo({ export async function getPersonalInfo({
signal, signal,
}: CallOptions = {}): Promise<UserRepresentation> { context,
const response = await request("/?userProfileMetadata=true", { signal }); }: CallOptions): Promise<UserRepresentation> {
const response = await request("/?userProfileMetadata=true", context, {
signal,
});
return parseResponse<UserRepresentation>(response); return parseResponse<UserRepresentation>(response);
} }
export async function getSupportedLocales({ export async function getSupportedLocales({
signal, signal,
}: CallOptions = {}): Promise<string[]> { context,
const response = await request("/supportedLocales", { signal }); }: CallOptions): Promise<string[]> {
const response = await request("/supportedLocales", context, { signal });
return parseResponse<string[]>(response); return parseResponse<string[]>(response);
} }
export async function savePersonalInfo( export async function savePersonalInfo(
context: KeycloakContext,
info: UserRepresentation, info: UserRepresentation,
): Promise<void> { ): Promise<void> {
const response = await request("/", { body: info, method: "POST" }); const response = await request("/", context, { body: info, method: "POST" });
if (!response.ok) { if (!response.ok) {
const { errors } = await response.json(); const { errors } = await response.json();
throw errors; throw errors;
@ -49,10 +55,11 @@ export async function savePersonalInfo(
export async function getPermissionRequests( export async function getPermissionRequests(
resourceId: string, resourceId: string,
{ signal }: CallOptions = {}, { signal, context }: CallOptions,
): Promise<Permission[]> { ): Promise<Permission[]> {
const response = await request( const response = await request(
`/resources/${resourceId}/permissions/requests`, `/resources/${resourceId}/permissions/requests`,
context,
{ signal }, { signal },
); );
@ -61,65 +68,89 @@ export async function getPermissionRequests(
export async function getDevices({ export async function getDevices({
signal, signal,
context,
}: CallOptions): Promise<DeviceRepresentation[]> { }: CallOptions): Promise<DeviceRepresentation[]> {
const response = await request("/sessions/devices", { signal }); const response = await request("/sessions/devices", context, { signal });
return parseResponse<DeviceRepresentation[]>(response); return parseResponse<DeviceRepresentation[]>(response);
} }
export async function getApplications({ signal }: CallOptions = {}): Promise< export async function getApplications({
ClientRepresentation[] signal,
> { context,
const response = await request("/applications", { signal }); }: CallOptions): Promise<ClientRepresentation[]> {
const response = await request("/applications", context, { signal });
return parseResponse<ClientRepresentation[]>(response); return parseResponse<ClientRepresentation[]>(response);
} }
export async function deleteConsent(id: string) { export async function deleteConsent(context: KeycloakContext, id: string) {
return request(`/applications/${id}/consent`, { method: "DELETE" }); return request(`/applications/${id}/consent`, context, { method: "DELETE" });
} }
export async function deleteSession(id?: string) { export async function deleteSession(context: KeycloakContext, id?: string) {
return request(`/sessions${id ? `/${id}` : ""}`, { return request(`/sessions${id ? `/${id}` : ""}`, context, {
method: "DELETE", method: "DELETE",
}); });
} }
export async function getCredentials({ signal }: CallOptions) { export async function getCredentials({ signal, context }: CallOptions) {
const response = await request("/credentials", { const response = await request("/credentials", context, {
signal, signal,
}); });
return parseResponse<CredentialContainer[]>(response); return parseResponse<CredentialContainer[]>(response);
} }
export async function deleteCredentials(credential: CredentialRepresentation) { export async function deleteCredentials(
return request("/credentials/" + credential.id, { context: KeycloakContext,
credential: CredentialRepresentation,
) {
return request("/credentials/" + credential.id, context, {
method: "DELETE", method: "DELETE",
}); });
} }
export async function getLinkedAccounts({ signal }: CallOptions) { export async function getLinkedAccounts({ signal, context }: CallOptions) {
const response = await request("/linked-accounts", { signal }); const response = await request("/linked-accounts", context, { signal });
return parseResponse<LinkedAccountRepresentation[]>(response); return parseResponse<LinkedAccountRepresentation[]>(response);
} }
export async function unLinkAccount(account: LinkedAccountRepresentation) { export async function unLinkAccount(
const response = await request("/linked-accounts/" + account.providerName, { context: KeycloakContext,
method: "DELETE", account: LinkedAccountRepresentation,
}); ) {
const response = await request(
"/linked-accounts/" + account.providerName,
context,
{
method: "DELETE",
},
);
return parseResponse(response); return parseResponse(response);
} }
export async function linkAccount(account: LinkedAccountRepresentation) { export async function linkAccount(
context: KeycloakContext,
account: LinkedAccountRepresentation,
) {
const redirectUri = encodeURIComponent( 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); return parseResponse<{ accountLinkUri: string }>(response);
} }
export async function getGroups({ signal }: CallOptions) { export async function getGroups({ signal, context }: CallOptions) {
const response = await request("/groups", { const response = await request("/groups", context, {
signal, signal,
}); });
return parseResponse<Group[]>(response); return parseResponse<Group[]>(response);

View file

@ -1,23 +1,21 @@
import { environment } from "../environment"; import { Environment } from "../environment";
import { keycloak } from "../keycloak"; import Keycloak from "keycloak-js";
import { joinPath } from "../utils/joinPath";
import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants"; import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants";
import { joinPath } from "../utils/joinPath";
import { KeycloakContext } from "../root/KeycloakContext";
export type RequestOptions = { export type RequestOptions = {
signal?: AbortSignal; signal?: AbortSignal;
getAccessToken?: () => Promise<string | undefined>;
method?: "POST" | "PUT" | "DELETE"; method?: "POST" | "PUT" | "DELETE";
searchParams?: Record<string, string>; searchParams?: Record<string, string>;
body?: unknown; body?: unknown;
}; };
export async function request( async function _request(
path: string, url: URL,
{ signal, method, searchParams, body }: RequestOptions = {}, { signal, getAccessToken, method, searchParams, body }: RequestOptions = {},
): Promise<Response> { ): Promise<Response> {
const url = new URL(
joinPath(environment.authUrl, "realms", environment.realm, "account", path),
);
if (searchParams) { if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => Object.entries(searchParams).forEach(([key, value]) =>
url.searchParams.set(key, value), url.searchParams.set(key, value),
@ -30,17 +28,34 @@ export async function request(
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
headers: { headers: {
[CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON, [CONTENT_TYPE_HEADER]: CONTENT_TYPE_JSON,
authorization: `Bearer ${await getAccessToken()}`, authorization: `Bearer ${await getAccessToken?.()}`,
}, },
}); });
} }
async function getAccessToken() { export async function request(
try { path: string,
await keycloak.updateToken(5); { environment, keycloak }: KeycloakContext,
} catch (error) { opts: RequestOptions = {},
await keycloak.login(); ) {
} return _request(url(environment, path), {
...opts,
return keycloak.token; 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;
};

View file

@ -27,6 +27,7 @@ import { deleteConsent, getApplications } from "../api/methods";
import { ClientRepresentation } from "../api/representations"; import { ClientRepresentation } from "../api/representations";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import { TFuncKey } from "../i18n"; import { TFuncKey } from "../i18n";
import { useEnvironment } from "../root/KeycloakContext";
import { formatDate } from "../utils/formatDate"; import { formatDate } from "../utils/formatDate";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
@ -34,8 +35,9 @@ type Application = ClientRepresentation & {
open: boolean; open: boolean;
}; };
const Applications = () => { export const Applications = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [applications, setApplications] = useState<Application[]>(); const [applications, setApplications] = useState<Application[]>();
@ -43,7 +45,7 @@ const Applications = () => {
const refresh = () => setKey(key + 1); const refresh = () => setKey(key + 1);
usePromise( usePromise(
(signal) => getApplications({ signal }), (signal) => getApplications({ signal, context }),
(clients) => setApplications(clients.map((c) => ({ ...c, open: false }))), (clients) => setApplications(clients.map((c) => ({ ...c, open: false }))),
[key], [key],
); );
@ -58,7 +60,7 @@ const Applications = () => {
const removeConsent = async (id: string) => { const removeConsent = async (id: string) => {
try { try {
await deleteConsent(id); await deleteConsent(context, id);
refresh(); refresh();
addAlert(t("removeConsentSuccess")); addAlert(t("removeConsentSuccess"));
} catch (error) { } catch (error) {

View file

@ -1,12 +1,12 @@
import { Spinner } from "@patternfly/react-core"; import { Spinner } from "@patternfly/react-core";
import { Suspense, lazy, useMemo, useState } from "react"; import { Suspense, lazy, useMemo, useState } from "react";
import { useParams } from "react-router-dom"; 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 { ContentComponentParams } from "../routes";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
import fetchContentJson from "./fetchContent"; import fetchContentJson from "./fetchContent";
import { MenuItem } from "../root/PageNav";
function findComponent( function findComponent(
content: MenuItem[], content: MenuItem[],
@ -27,11 +27,13 @@ function findComponent(
return undefined; return undefined;
} }
const ContentComponent = () => { export const ContentComponent = () => {
const context = useEnvironment();
const [content, setContent] = useState<MenuItem[]>(); const [content, setContent] = useState<MenuItem[]>();
const { componentId } = useParams<ContentComponentParams>(); const { componentId } = useParams<ContentComponentParams>();
usePromise((signal) => fetchContentJson({ signal }), setContent); usePromise((signal) => fetchContentJson({ signal, context }), setContent);
const modulePath = useMemo( const modulePath = useMemo(
() => findComponent(content || [], componentId!), () => findComponent(content || [], componentId!),
[content, componentId], [content, componentId],
@ -45,6 +47,8 @@ type ComponentProps = {
}; };
const Component = ({ modulePath }: ComponentProps) => { const Component = ({ modulePath }: ComponentProps) => {
const { environment } = useEnvironment();
const Element = lazy( const Element = lazy(
() => import(joinPath(environment.resourceUrl, modulePath)), () => import(joinPath(environment.resourceUrl, modulePath)),
); );

View file

@ -1,13 +1,12 @@
import { CallOptions } from "../api/methods"; import { CallOptions } from "../api/methods";
import { environment } from "../environment";
import { MenuItem } from "../root/PageNav"; import { MenuItem } from "../root/PageNav";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
export default async function fetchContentJson( export default async function fetchContentJson(
opts: CallOptions = {}, opts: CallOptions,
): Promise<MenuItem[]> { ): Promise<MenuItem[]> {
const response = await fetch( const response = await fetch(
joinPath(environment.resourceUrl, "/content.json"), joinPath(opts.context.environment.resourceUrl, "/content.json"),
opts, opts,
); );
return await response.json(); return await response.json();

View file

@ -11,16 +11,18 @@ import { useTranslation } from "react-i18next";
import { getGroups } from "../api/methods"; import { getGroups } from "../api/methods";
import { Group } from "../api/representations"; import { Group } from "../api/representations";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import { useEnvironment } from "../root/KeycloakContext";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
const Groups = () => { export const Groups = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const [directMembership, setDirectMembership] = useState(false); const [directMembership, setDirectMembership] = useState(false);
usePromise( usePromise(
(signal) => getGroups({ signal }), (signal) => getGroups({ signal, context }),
(groups) => { (groups) => {
if (directMembership) { if (directMembership) {
groups.forEach((el) => groups.forEach((el) =>

View 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";

View file

@ -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,
});

View file

@ -6,16 +6,10 @@ import { createRoot } from "react-dom/client";
import { createHashRouter, RouterProvider } from "react-router-dom"; import { createHashRouter, RouterProvider } from "react-router-dom";
import { i18n } from "./i18n"; import { i18n } from "./i18n";
import { keycloak } from "./keycloak";
import { routes } from "./routes"; import { routes } from "./routes";
// Initialize required components before rendering app. // Initialize required components before rendering app.
await Promise.all([ await i18n.init();
keycloak.init({
onLoad: "check-sso",
}),
i18n.init(),
]);
const router = createHashRouter(routes); const router = createHashRouter(routes);
const container = document.getElementById("app"); const container = document.getElementById("app");

View file

@ -28,12 +28,13 @@ import {
UserRepresentation, UserRepresentation,
} from "../api/representations"; } from "../api/representations";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import { environment } from "../environment";
import { TFuncKey, i18n } from "../i18n"; import { TFuncKey, i18n } from "../i18n";
import { useEnvironment } from "../root/KeycloakContext";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
const PersonalInfo = () => { export const PersonalInfo = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const keycloak = useKeycloak(); const keycloak = useKeycloak();
const [userProfileMetadata, setUserProfileMetadata] = const [userProfileMetadata, setUserProfileMetadata] =
useState<UserProfileMetadata>(); useState<UserProfileMetadata>();
@ -45,8 +46,8 @@ const PersonalInfo = () => {
usePromise( usePromise(
(signal) => (signal) =>
Promise.all([ Promise.all([
getPersonalInfo({ signal }), getPersonalInfo({ signal, context }),
getSupportedLocales({ signal }), getSupportedLocales({ signal, context }),
]), ]),
([personalInfo, supportedLocales]) => { ([personalInfo, supportedLocales]) => {
setUserProfileMetadata(personalInfo.userProfileMetadata); setUserProfileMetadata(personalInfo.userProfileMetadata);
@ -57,7 +58,7 @@ const PersonalInfo = () => {
const onSubmit = async (user: UserRepresentation) => { const onSubmit = async (user: UserRepresentation) => {
try { try {
await savePersonalInfo(user); await savePersonalInfo(context, user);
const locale = user.attributes?.["locale"]?.toString(); const locale = user.attributes?.["locale"]?.toString();
i18n.changeLanguage(locale, (error) => { i18n.changeLanguage(locale, (error) => {
if (error) { if (error) {
@ -87,7 +88,7 @@ const PersonalInfo = () => {
updateEmailActionEnabled, updateEmailActionEnabled,
isRegistrationEmailAsUsername, isRegistrationEmailAsUsername,
isEditUserNameAllowed, isEditUserNameAllowed,
} = environment.features; } = context.environment.features;
return ( return (
<Page title={t("personalInfo")} description={t("personalInfoDescription")}> <Page title={t("personalInfo")} description={t("personalInfoDescription")}>
<Form isHorizontal onSubmit={handleSubmit(onSubmit)}> <Form isHorizontal onSubmit={handleSubmit(onSubmit)}>
@ -136,7 +137,7 @@ const PersonalInfo = () => {
{t("cancel")} {t("cancel")}
</Button> </Button>
</ActionGroup> </ActionGroup>
{environment.features.deleteAccountAllowed && ( {context.environment.features.deleteAccountAllowed && (
<ExpandableSection <ExpandableSection
data-testid="delete-account" data-testid="delete-account"
toggleText={t("deleteAccount")} toggleText={t("deleteAccount")}

View file

@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { KeycloakTextInput, SelectControl, useAlerts } from "ui-shared"; import { KeycloakTextInput, SelectControl, useAlerts } from "ui-shared";
import { updatePermissions } from "../api"; import { updatePermissions } from "../api";
import type { Permission, Resource } from "../api/representations"; import type { Permission, Resource } from "../api/representations";
import { useEnvironment } from "../root/KeycloakContext";
type EditTheResourceProps = { type EditTheResourceProps = {
resource: Resource; resource: Resource;
@ -23,6 +24,7 @@ export const EditTheResource = ({
onClose, onClose,
}: EditTheResourceProps) => { }: EditTheResourceProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const form = useForm<FormValues>(); const form = useForm<FormValues>();
@ -39,7 +41,7 @@ export const EditTheResource = ({
try { try {
await Promise.all( await Promise.all(
permissions.map((permission) => permissions.map((permission) =>
updatePermissions(resource._id, [permission]), updatePermissions(context, resource._id, [permission]),
), ),
); );
addAlert(t("updateSuccess")); addAlert(t("updateSuccess"));

View file

@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
import { useAlerts } from "ui-shared"; import { useAlerts } from "ui-shared";
import { fetchPermission, updateRequest } from "../api"; import { fetchPermission, updateRequest } from "../api";
import { Permission, Resource } from "../api/representations"; import { Permission, Resource } from "../api/representations";
import { useEnvironment } from "../root/KeycloakContext";
type PermissionRequestProps = { type PermissionRequestProps = {
resource: Resource; resource: Resource;
@ -32,6 +33,7 @@ export const PermissionRequest = ({
refresh, refresh,
}: PermissionRequestProps) => { }: PermissionRequestProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -43,12 +45,13 @@ export const PermissionRequest = ({
approve: boolean = false, approve: boolean = false,
) => { ) => {
try { try {
const permissions = await fetchPermission({}, resource._id); const permissions = await fetchPermission({ context }, resource._id);
const { scopes, username } = permissions.find( const { scopes, username } = permissions.find(
(p) => p.username === shareRequest.username, (p) => p.username === shareRequest.username,
)!; )!;
await updateRequest( await updateRequest(
context,
resource._id, resource._id,
username, username,
approve approve

View file

@ -5,7 +5,7 @@ import { Tab, Tabs, TabTitleText } from "@patternfly/react-core";
import { ResourcesTab } from "./ResourcesTab"; import { ResourcesTab } from "./ResourcesTab";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
const Resources = () => { export const Resources = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTabKey, setActiveTabKey] = useState(0); const [activeTabKey, setActiveTabKey] = useState(0);

View file

@ -31,17 +31,18 @@ import {
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ContinueCancelModal, useAlerts } from "ui-shared";
import { fetchPermission, fetchResources, updatePermissions } from "../api"; import { fetchPermission, fetchResources, updatePermissions } from "../api";
import { getPermissionRequests } from "../api/methods"; import { getPermissionRequests } from "../api/methods";
import { Links } from "../api/parse-links"; import { Links } from "../api/parse-links";
import { Permission, Resource } from "../api/representations"; import { Permission, Resource } from "../api/representations";
import { ContinueCancelModal, useAlerts } from "ui-shared"; import { useEnvironment } from "../root/KeycloakContext";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
import { EditTheResource } from "./EditTheResource"; import { EditTheResource } from "./EditTheResource";
import { PermissionRequest } from "./PermissionRequest"; import { PermissionRequest } from "./PermissionRequest";
import { ResourceToolbar } from "./ResourceToolbar"; import { ResourceToolbar } from "./ResourceToolbar";
import { SharedWith } from "./SharedWith";
import { ShareTheResource } from "./ShareTheResource"; import { ShareTheResource } from "./ShareTheResource";
import { SharedWith } from "./SharedWith";
type PermissionDetail = { type PermissionDetail = {
contextOpen?: boolean; contextOpen?: boolean;
@ -57,6 +58,7 @@ type ResourcesTabProps = {
export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => { export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [params, setParams] = useState<Record<string, string>>({ const [params, setParams] = useState<Record<string, string>>({
@ -73,13 +75,18 @@ export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
usePromise( usePromise(
async (signal) => { async (signal) => {
const result = await fetchResources({ signal }, params, isShared); const result = await fetchResources(
{ signal, context },
params,
isShared,
);
if (!isShared) if (!isShared)
await Promise.all( await Promise.all(
result.data.map( result.data.map(
async (r) => async (r) =>
(r.shareRequests = await getPermissionRequests(r._id, { (r.shareRequests = await getPermissionRequests(r._id, {
signal, signal,
context,
})), })),
), ),
); );
@ -99,7 +106,7 @@ export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
const fetchPermissions = async (id: string) => { const fetchPermissions = async (id: string) => {
let permissions = details[id]?.permissions || []; let permissions = details[id]?.permissions || [];
if (!details[id]) { if (!details[id]) {
permissions = await fetchPermission({}, id); permissions = await fetchPermission({ context }, id);
} }
return permissions; return permissions;
}; };
@ -113,7 +120,7 @@ export const ResourcesTab = ({ isShared = false }: ResourcesTabProps) => {
scopes: [], scopes: [],
}) as Permission, }) as Permission,
)!; )!;
await updatePermissions(resource._id, permissions); await updatePermissions(context, resource._id, permissions);
setDetails({}); setDetails({});
addAlert(t("unShareSuccess")); addAlert(t("unShareSuccess"));
} catch (error) { } catch (error) {

View file

@ -17,9 +17,10 @@ import {
} from "react-hook-form"; } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { KeycloakTextInput, SelectControl, useAlerts } from "ui-shared";
import { updateRequest } from "../api"; import { updateRequest } from "../api";
import { Permission, Resource } from "../api/representations"; import { Permission, Resource } from "../api/representations";
import { useAlerts, SelectControl, KeycloakTextInput } from "ui-shared"; import { useEnvironment } from "../root/KeycloakContext";
import { SharedWith } from "./SharedWith"; import { SharedWith } from "./SharedWith";
type ShareTheResourceProps = { type ShareTheResourceProps = {
@ -41,6 +42,7 @@ export const ShareTheResource = ({
onClose, onClose,
}: ShareTheResourceProps) => { }: ShareTheResourceProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const form = useForm<FormValues>(); const form = useForm<FormValues>();
const { const {
@ -79,7 +81,7 @@ export const ShareTheResource = ({
usernames usernames
.filter(({ value }) => value !== "") .filter(({ value }) => value !== "")
.map(({ value: username }) => .map(({ value: username }) =>
updateRequest(resource._id, username, permissions), updateRequest(context, resource._id, username, permissions),
), ),
); );
addAlert(t("shareSuccess")); addAlert(t("shareSuccess"));

View 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>
);
};

View 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>
);
};

View file

@ -22,29 +22,31 @@ import {
useLocation, useLocation,
} from "react-router-dom"; } from "react-router-dom";
import fetchContentJson from "../content/fetchContent"; import fetchContentJson from "../content/fetchContent";
import type { Feature } from "../environment";
import { TFuncKey } from "../i18n"; import { TFuncKey } from "../i18n";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
import { Feature, environment } from "../environment"; import { useEnvironment } from "./KeycloakContext";
type RootMenuItem = { type RootMenuItem = {
label: TFuncKey; label: TFuncKey;
path: string; path: string;
isHidden?: keyof Feature; isVisible?: keyof Feature;
modulePath?: string; modulePath?: string;
}; };
type MenuItemWithChildren = { type MenuItemWithChildren = {
label: TFuncKey; label: TFuncKey;
children: MenuItem[]; children: MenuItem[];
isHidden?: keyof Feature; isVisible?: keyof Feature;
}; };
export type MenuItem = RootMenuItem | MenuItemWithChildren; export type MenuItem = RootMenuItem | MenuItemWithChildren;
export const PageNav = () => { export const PageNav = () => {
const [menuItems, setMenuItems] = useState<MenuItem[]>(); const [menuItems, setMenuItems] = useState<MenuItem[]>();
const context = useEnvironment();
usePromise((signal) => fetchContentJson({ signal }), setMenuItems); usePromise((signal) => fetchContentJson({ signal, context }), setMenuItems);
return ( return (
<PageSidebar <PageSidebar
nav={ nav={
@ -53,8 +55,8 @@ export const PageNav = () => {
<Suspense fallback={<Spinner />}> <Suspense fallback={<Spinner />}>
{menuItems {menuItems
?.filter((menuItem) => ?.filter((menuItem) =>
menuItem.isHidden menuItem.isVisible
? environment.features[menuItem.isHidden] ? context.environment.features[menuItem.isVisible]
: true, : true,
) )
.map((menuItem) => ( .map((menuItem) => (
@ -77,6 +79,9 @@ type NavMenuItemProps = {
function NavMenuItem({ menuItem }: NavMenuItemProps) { function NavMenuItem({ menuItem }: NavMenuItemProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const {
environment: { features },
} = useEnvironment();
const { pathname } = useLocation(); const { pathname } = useLocation();
const isActive = useMemo( const isActive = useMemo(
() => matchMenuItem(pathname, menuItem), () => matchMenuItem(pathname, menuItem),
@ -99,7 +104,9 @@ function NavMenuItem({ menuItem }: NavMenuItemProps) {
isExpanded={isActive} isExpanded={isActive}
> >
{menuItem.children {menuItem.children
.filter((menuItem) => !menuItem.isHidden) .filter((menuItem) =>
menuItem.isVisible ? features[menuItem.isVisible] : true,
)
.map((child) => ( .map((child) => (
<NavMenuItem key={child.label as string} menuItem={child} /> <NavMenuItem key={child.label as string} menuItem={child} />
))} ))}

View file

@ -1,89 +1,18 @@
import { Button, Page, Spinner } from "@patternfly/react-core"; import { Page, Spinner } from "@patternfly/react-core";
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons"; import { Suspense } from "react";
import { import { Outlet } from "react-router-dom";
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 { environment } from "../environment"; import { environment } from "../environment";
import { keycloak } from "../keycloak"; import { Header } from "./Header";
import { joinPath } from "../utils/joinPath"; import { KeycloakProvider } from "./KeycloakContext";
import { PageNav } from "./PageNav"; 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 = () => { 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 ( return (
<KeycloakProvider keycloak={keycloak}> <KeycloakProvider environment={environment}>
<Page <Page header={<Header />} sidebar={<PageNav />} isManagedSidebar>
header={ <Suspense fallback={<Spinner />}>
<TranslationsProvider translations={translations}> <Outlet />
<KeycloakMasthead </Suspense>
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>
</Page> </Page>
</KeycloakProvider> </KeycloakProvider>
); );

View file

@ -3,11 +3,7 @@ import { login } from "./login";
test("Check page heading", async ({ page }) => { test("Check page heading", async ({ page }) => {
await login(page, "alice", "alice", "user-profile"); await login(page, "alice", "alice", "user-profile");
await page await page.getByTestId("accountSecurity").click();
.getByRole("button", {
name: "Account security",
})
.click();
const linkedAccountsNavItem = page.getByTestId( const linkedAccountsNavItem = page.getByTestId(
"account-security/linked-accounts", "account-security/linked-accounts",

View file

@ -1,21 +1,36 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path";
import { defineConfig, loadEnv } from "vite";
import { checker } from "vite-plugin-checker"; import { checker } from "vite-plugin-checker";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
base: "", const env = loadEnv(mode, process.cwd(), "");
server: { const external = ["react", "react/jsx-runtime", "react-dom"];
port: 8080, if (env.LIB) external.push("react-router-dom");
}, const lib = env.LIB
build: { ? {
sourcemap: true, lib: {
target: "esnext", entry: path.resolve(__dirname, "src/index.ts"),
modulePreload: false, formats: ["es"],
cssMinify: "lightningcss", },
rollupOptions: { }
external: ["react", "react/jsx-runtime", "react-dom"], : undefined;
return {
base: "",
server: {
port: 8080,
}, },
}, build: {
plugins: [react(), checker({ typescript: true })], ...lib,
sourcemap: true,
target: "esnext",
modulePreload: false,
cssMinify: "lightningcss",
rollupOptions: {
external: external,
},
},
plugins: [react(), checker({ typescript: true })],
};
}); });