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",
|
"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",
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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 }),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
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 { 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");
|
||||||
|
|
|
@ -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")}
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
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,
|
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} />
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 })],
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue