@keycloakify/keycloak-ui-shared 26.0.6001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -0
- package/README.md +6 -0
- package/keycloak-theme/shared/keycloak-ui-shared/alerts/AlertPanel.tsx +43 -0
- package/keycloak-theme/shared/keycloak-ui-shared/alerts/Alerts.tsx +82 -0
- package/keycloak-theme/shared/keycloak-ui-shared/buttons/FormSubmitButton.tsx +47 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +60 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/HelpContext.tsx +30 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +97 -0
- package/keycloak-theme/shared/keycloak-ui-shared/context/environment.ts +50 -0
- package/keycloak-theme/shared/keycloak-ui-shared/continue-cancel/ContinueCancelModal.tsx +75 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/FormErrorText.tsx +23 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +40 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/HelpItem.tsx +43 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/KeycloakSpinner.tsx +12 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/NumberControl.tsx +93 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +122 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordControl.tsx +71 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordInput.tsx +50 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +67 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +60 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +75 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/keycloak-text-area/KeycloakTextArea.tsx +23 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +75 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +109 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +285 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +597 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +86 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/PaginatingTableToolbar.tsx +106 -0
- package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +92 -0
- package/keycloak-theme/shared/keycloak-ui-shared/icons/IconMapper.tsx +63 -0
- package/keycloak-theme/shared/keycloak-ui-shared/index.ts +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/main.ts +96 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/DefaultAvatar.tsx +109 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/KeycloakDropdown.tsx +48 -0
- package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +161 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormPanel.tsx +29 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormTitle.tsx +28 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +98 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollPanel.tsx +21 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/form-title.module.css +4 -0
- package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/scroll-form.module.css +8 -0
- package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +49 -0
- package/keycloak-theme/shared/keycloak-ui-shared/select/SingleSelect.tsx +89 -0
- package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +198 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +51 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/MultiInputComponent.tsx +146 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/OptionsComponent.tsx +63 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/SelectComponent.tsx +109 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextAreaComponent.tsx +23 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +32 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileFields.tsx +243 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileGroup.tsx +71 -0
- package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +170 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/ErrorBoundary.tsx +77 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/createNamedContext.ts +11 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/darkMode.ts +19 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/errors.ts +55 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/generateId.ts +1 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/getRuleValue.ts +17 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/isDefined.ts +3 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useFetch.ts +44 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useRequiredContext.ts +24 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useSetTimeout.ts +40 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useStorageItem.ts +51 -0
- package/keycloak-theme/shared/keycloak-ui-shared/utils/useStoredState.ts +38 -0
- package/package.json +31 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertGroup,
|
|
3
|
+
Alert,
|
|
4
|
+
AlertActionCloseButton,
|
|
5
|
+
AlertVariant,
|
|
6
|
+
} from "@patternfly/react-core";
|
|
7
|
+
|
|
8
|
+
import type { AlertEntry } from "./Alerts";
|
|
9
|
+
|
|
10
|
+
export type AlertPanelProps = {
|
|
11
|
+
alerts: AlertEntry[];
|
|
12
|
+
onCloseAlert: (id: number) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function AlertPanel({ alerts, onCloseAlert }: AlertPanelProps) {
|
|
16
|
+
return (
|
|
17
|
+
<AlertGroup
|
|
18
|
+
data-testid="global-alerts"
|
|
19
|
+
isToast
|
|
20
|
+
style={{ whiteSpace: "pre-wrap" }}
|
|
21
|
+
>
|
|
22
|
+
{alerts.map(({ id, variant, message, description }, index) => (
|
|
23
|
+
<Alert
|
|
24
|
+
key={id}
|
|
25
|
+
data-testid={index === 0 ? "last-alert" : undefined}
|
|
26
|
+
isLiveRegion
|
|
27
|
+
variant={AlertVariant[variant]}
|
|
28
|
+
component="p"
|
|
29
|
+
variantLabel=""
|
|
30
|
+
title={message}
|
|
31
|
+
actionClose={
|
|
32
|
+
<AlertActionCloseButton
|
|
33
|
+
title={message}
|
|
34
|
+
onClose={() => onCloseAlert(id)}
|
|
35
|
+
/>
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{description && <p>{description}</p>}
|
|
39
|
+
</Alert>
|
|
40
|
+
))}
|
|
41
|
+
</AlertGroup>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AlertVariant } from "@patternfly/react-core";
|
|
2
|
+
import { PropsWithChildren, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
|
|
5
|
+
import { createNamedContext } from "../utils/createNamedContext";
|
|
6
|
+
import { getErrorDescription, getErrorMessage } from "../utils/errors";
|
|
7
|
+
import { generateId } from "../utils/generateId";
|
|
8
|
+
import { useRequiredContext } from "../utils/useRequiredContext";
|
|
9
|
+
import { useSetTimeout } from "../utils/useSetTimeout";
|
|
10
|
+
import { AlertPanel } from "./AlertPanel";
|
|
11
|
+
|
|
12
|
+
const ALERT_TIMEOUT = 8000;
|
|
13
|
+
|
|
14
|
+
export type AddAlertFunction = (
|
|
15
|
+
message: string,
|
|
16
|
+
variant?: AlertVariant,
|
|
17
|
+
description?: string,
|
|
18
|
+
) => void;
|
|
19
|
+
|
|
20
|
+
export type AddErrorFunction = (messageKey: string, error: unknown) => void;
|
|
21
|
+
|
|
22
|
+
export type AlertProps = {
|
|
23
|
+
addAlert: AddAlertFunction;
|
|
24
|
+
addError: AddErrorFunction;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const AlertContext = createNamedContext<AlertProps | undefined>(
|
|
28
|
+
"AlertContext",
|
|
29
|
+
undefined,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const useAlerts = () => useRequiredContext(AlertContext);
|
|
33
|
+
|
|
34
|
+
export type AlertEntry = {
|
|
35
|
+
id: number;
|
|
36
|
+
message: string;
|
|
37
|
+
variant: AlertVariant;
|
|
38
|
+
description?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const AlertProvider = ({ children }: PropsWithChildren) => {
|
|
42
|
+
const { t } = useTranslation();
|
|
43
|
+
const setTimeout = useSetTimeout();
|
|
44
|
+
const [alerts, setAlerts] = useState<AlertEntry[]>([]);
|
|
45
|
+
|
|
46
|
+
const removeAlert = (id: number) =>
|
|
47
|
+
setAlerts((alerts) => alerts.filter((alert) => alert.id !== id));
|
|
48
|
+
|
|
49
|
+
const addAlert = useCallback<AddAlertFunction>(
|
|
50
|
+
(message, variant = AlertVariant.success, description) => {
|
|
51
|
+
const alert: AlertEntry = {
|
|
52
|
+
id: generateId(),
|
|
53
|
+
message,
|
|
54
|
+
variant,
|
|
55
|
+
description,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
setAlerts((alerts) => [alert, ...alerts]);
|
|
59
|
+
setTimeout(() => removeAlert(alert.id), ALERT_TIMEOUT);
|
|
60
|
+
},
|
|
61
|
+
[setTimeout],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const addError = useCallback<AddErrorFunction>(
|
|
65
|
+
(messageKey, error) => {
|
|
66
|
+
const message = t(messageKey, { error: getErrorMessage(error) });
|
|
67
|
+
const description = getErrorDescription(error);
|
|
68
|
+
|
|
69
|
+
addAlert(message, AlertVariant.danger, description);
|
|
70
|
+
},
|
|
71
|
+
[addAlert, t],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const value = useMemo(() => ({ addAlert, addError }), [addAlert, addError]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<AlertContext.Provider value={value}>
|
|
78
|
+
<AlertPanel alerts={alerts} onCloseAlert={removeAlert} />
|
|
79
|
+
{children}
|
|
80
|
+
</AlertContext.Provider>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Button, ButtonProps } from "@patternfly/react-core";
|
|
2
|
+
import { PropsWithChildren } from "react";
|
|
3
|
+
import { FieldValues, FormState } from "react-hook-form";
|
|
4
|
+
|
|
5
|
+
export type FormSubmitButtonProps = Omit<ButtonProps, "isDisabled"> & {
|
|
6
|
+
formState: FormState<FieldValues>;
|
|
7
|
+
allowNonDirty?: boolean;
|
|
8
|
+
allowInvalid?: boolean;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const isSubmittable = (
|
|
13
|
+
formState: FormState<FieldValues>,
|
|
14
|
+
allowNonDirty: boolean,
|
|
15
|
+
allowInvalid: boolean,
|
|
16
|
+
) => {
|
|
17
|
+
return (
|
|
18
|
+
(formState.isValid || allowInvalid) &&
|
|
19
|
+
(formState.isDirty || allowNonDirty) &&
|
|
20
|
+
!formState.isLoading &&
|
|
21
|
+
!formState.isValidating &&
|
|
22
|
+
!formState.isSubmitting
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const FormSubmitButton = ({
|
|
27
|
+
formState,
|
|
28
|
+
isDisabled = false,
|
|
29
|
+
allowInvalid = false,
|
|
30
|
+
allowNonDirty = false,
|
|
31
|
+
children,
|
|
32
|
+
...rest
|
|
33
|
+
}: PropsWithChildren<FormSubmitButtonProps>) => {
|
|
34
|
+
return (
|
|
35
|
+
<Button
|
|
36
|
+
variant="primary"
|
|
37
|
+
isDisabled={
|
|
38
|
+
(formState && !isSubmittable(formState, allowNonDirty, allowInvalid)) ||
|
|
39
|
+
isDisabled
|
|
40
|
+
}
|
|
41
|
+
{...rest}
|
|
42
|
+
type="submit"
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</Button>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Modal,
|
|
4
|
+
ModalVariant,
|
|
5
|
+
Page,
|
|
6
|
+
Text,
|
|
7
|
+
TextContent,
|
|
8
|
+
TextVariants,
|
|
9
|
+
} from "@patternfly/react-core";
|
|
10
|
+
import { useTranslation } from "react-i18next";
|
|
11
|
+
|
|
12
|
+
type ErrorPageProps = {
|
|
13
|
+
error?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ErrorPage = (props: ErrorPageProps) => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const error = props.error;
|
|
19
|
+
const errorMessage = getErrorMessage(error);
|
|
20
|
+
|
|
21
|
+
function onRetry() {
|
|
22
|
+
location.href = location.origin + location.pathname;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Page>
|
|
27
|
+
<Modal
|
|
28
|
+
variant={ModalVariant.small}
|
|
29
|
+
title={t("somethingWentWrong")}
|
|
30
|
+
titleIconVariant="danger"
|
|
31
|
+
showClose={false}
|
|
32
|
+
isOpen
|
|
33
|
+
actions={[
|
|
34
|
+
<Button key="tryAgain" variant="primary" onClick={onRetry}>
|
|
35
|
+
{t("tryAgain")}
|
|
36
|
+
</Button>,
|
|
37
|
+
]}
|
|
38
|
+
>
|
|
39
|
+
<TextContent>
|
|
40
|
+
<Text>{t("somethingWentWrongDescription")}</Text>
|
|
41
|
+
{errorMessage && (
|
|
42
|
+
<Text component={TextVariants.small}>{errorMessage}</Text>
|
|
43
|
+
)}
|
|
44
|
+
</TextContent>
|
|
45
|
+
</Modal>
|
|
46
|
+
</Page>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function getErrorMessage(error: unknown): string | null {
|
|
51
|
+
if (typeof error === "string") {
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (error instanceof Error) {
|
|
56
|
+
return error.message;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PropsWithChildren } from "react";
|
|
2
|
+
import { createNamedContext } from "../utils/createNamedContext";
|
|
3
|
+
import { useRequiredContext } from "../utils/useRequiredContext";
|
|
4
|
+
import { useStoredState } from "../utils/useStoredState";
|
|
5
|
+
|
|
6
|
+
type HelpContextProps = {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
toggleHelp: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const HelpContext = createNamedContext<HelpContextProps | undefined>(
|
|
12
|
+
"HelpContext",
|
|
13
|
+
undefined,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export const useHelp = () => useRequiredContext(HelpContext);
|
|
17
|
+
|
|
18
|
+
export const Help = ({ children }: PropsWithChildren) => {
|
|
19
|
+
const [enabled, setHelp] = useStoredState(localStorage, "helpEnabled", true);
|
|
20
|
+
|
|
21
|
+
function toggleHelp() {
|
|
22
|
+
setHelp(!enabled);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<HelpContext.Provider value={{ enabled, toggleHelp }}>
|
|
27
|
+
{children}
|
|
28
|
+
</HelpContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Spinner } from "@patternfly/react-core";
|
|
2
|
+
import Keycloak from "keycloak-js";
|
|
3
|
+
import {
|
|
4
|
+
PropsWithChildren,
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { AlertProvider } from "../alerts/Alerts";
|
|
13
|
+
import { ErrorPage } from "./ErrorPage";
|
|
14
|
+
import { Help } from "./HelpContext";
|
|
15
|
+
import { BaseEnvironment } from "./environment";
|
|
16
|
+
|
|
17
|
+
export type KeycloakContext<T extends BaseEnvironment = BaseEnvironment> =
|
|
18
|
+
KeycloakContextProps<T> & {
|
|
19
|
+
keycloak: Keycloak;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const createKeycloakEnvContext = <T extends BaseEnvironment>() =>
|
|
23
|
+
createContext<KeycloakContext<T> | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
let KeycloakEnvContext: any;
|
|
26
|
+
|
|
27
|
+
export const useEnvironment = <
|
|
28
|
+
T extends BaseEnvironment = BaseEnvironment,
|
|
29
|
+
>() => {
|
|
30
|
+
const context = useContext<KeycloakContext<T>>(KeycloakEnvContext);
|
|
31
|
+
if (!context)
|
|
32
|
+
throw Error(
|
|
33
|
+
"no environment provider in the hierarchy make sure to add the provider",
|
|
34
|
+
);
|
|
35
|
+
return context;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface KeycloakContextProps<T extends BaseEnvironment> {
|
|
39
|
+
environment: T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const KeycloakProvider = <T extends BaseEnvironment>({
|
|
43
|
+
environment,
|
|
44
|
+
children,
|
|
45
|
+
}: PropsWithChildren<KeycloakContextProps<T>>) => {
|
|
46
|
+
KeycloakEnvContext = createKeycloakEnvContext<T>();
|
|
47
|
+
const calledOnce = useRef(false);
|
|
48
|
+
const [init, setInit] = useState(false);
|
|
49
|
+
const [error, setError] = useState<unknown>();
|
|
50
|
+
const keycloak = useMemo(() => {
|
|
51
|
+
const keycloak = new Keycloak({
|
|
52
|
+
url: environment.serverBaseUrl,
|
|
53
|
+
realm: environment.realm,
|
|
54
|
+
clientId: environment.clientId,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
keycloak.onAuthLogout = () => keycloak.login();
|
|
58
|
+
|
|
59
|
+
return keycloak;
|
|
60
|
+
}, [environment]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
// only needed in dev mode
|
|
64
|
+
if (calledOnce.current) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const init = () =>
|
|
69
|
+
keycloak.init({
|
|
70
|
+
onLoad: "check-sso",
|
|
71
|
+
pkceMethod: "S256",
|
|
72
|
+
responseMode: "query",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
init()
|
|
76
|
+
.then(() => setInit(true))
|
|
77
|
+
.catch((error) => setError(error));
|
|
78
|
+
|
|
79
|
+
calledOnce.current = true;
|
|
80
|
+
}, [keycloak]);
|
|
81
|
+
|
|
82
|
+
if (error) {
|
|
83
|
+
return <ErrorPage error={error} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!init) {
|
|
87
|
+
return <Spinner />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<KeycloakEnvContext.Provider value={{ environment, keycloak }}>
|
|
92
|
+
<AlertProvider>
|
|
93
|
+
<Help>{children}</Help>
|
|
94
|
+
</AlertProvider>
|
|
95
|
+
</KeycloakEnvContext.Provider>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** The base environment variables that are shared between the Admin and Account Consoles. */
|
|
2
|
+
export type BaseEnvironment = {
|
|
3
|
+
/**
|
|
4
|
+
* The URL to the root of the Keycloak server, including the path if present, this is **NOT** always equivalent to the URL of the Admin Console.
|
|
5
|
+
* For example, the Keycloak server could be hosted on `auth.example.com` and Admin Console may be hosted on `admin.example.com/some/path`.
|
|
6
|
+
*
|
|
7
|
+
* Note that this URL is normalized not to include a trailing slash, so take this into account when constructing URLs.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link https://www.keycloak.org/server/hostname#_administration_console}
|
|
10
|
+
*/
|
|
11
|
+
serverBaseUrl: string;
|
|
12
|
+
/** The identifier of the realm used to authenticate the user. */
|
|
13
|
+
realm: string;
|
|
14
|
+
/** The identifier of the client used to authenticate the user. */
|
|
15
|
+
clientId: string;
|
|
16
|
+
/** The base URL of the resources. */
|
|
17
|
+
resourceUrl: string;
|
|
18
|
+
/** The source URL for the the logo image. */
|
|
19
|
+
logo: string;
|
|
20
|
+
/** The URL to be followed when the logo is clicked. */
|
|
21
|
+
logoUrl: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the environment variables from the document, these variables are injected by Keycloak as a script tag, the contents of which can be parsed as JSON. For example:
|
|
26
|
+
*
|
|
27
|
+
*```html
|
|
28
|
+
* <script id="environment" type="application/json">
|
|
29
|
+
* {
|
|
30
|
+
* "realm": "master",
|
|
31
|
+
* "clientId": "security-admin-console",
|
|
32
|
+
* "etc": "..."
|
|
33
|
+
* }
|
|
34
|
+
* </script>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function getInjectedEnvironment<T>(): T {
|
|
38
|
+
const element = document.getElementById("environment");
|
|
39
|
+
const contents = element?.textContent;
|
|
40
|
+
|
|
41
|
+
if (typeof contents !== "string") {
|
|
42
|
+
throw new Error("Environment variables not found in the document.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(contents);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error("Unable to parse environment variables as JSON.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ReactNode, useState } from "react";
|
|
2
|
+
import { Button, ButtonProps, Modal, ModalProps } from "@patternfly/react-core";
|
|
3
|
+
|
|
4
|
+
export type ContinueCancelModalProps = Omit<ModalProps, "ref" | "children"> & {
|
|
5
|
+
modalTitle: string;
|
|
6
|
+
continueLabel: string;
|
|
7
|
+
cancelLabel: string;
|
|
8
|
+
buttonTitle: string | ReactNode;
|
|
9
|
+
buttonVariant?: ButtonProps["variant"];
|
|
10
|
+
buttonTestRole?: string;
|
|
11
|
+
isDisabled?: boolean;
|
|
12
|
+
onContinue: () => void;
|
|
13
|
+
component?: React.ElementType<any> | React.ComponentType<any>;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const ContinueCancelModal = ({
|
|
18
|
+
modalTitle,
|
|
19
|
+
continueLabel,
|
|
20
|
+
cancelLabel,
|
|
21
|
+
buttonTitle,
|
|
22
|
+
isDisabled,
|
|
23
|
+
buttonVariant,
|
|
24
|
+
buttonTestRole,
|
|
25
|
+
onContinue,
|
|
26
|
+
component = Button,
|
|
27
|
+
children,
|
|
28
|
+
...rest
|
|
29
|
+
}: ContinueCancelModalProps) => {
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const Component = component;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<Component
|
|
36
|
+
variant={buttonVariant}
|
|
37
|
+
onClick={() => setOpen(true)}
|
|
38
|
+
isDisabled={isDisabled}
|
|
39
|
+
data-testrole={buttonTestRole}
|
|
40
|
+
>
|
|
41
|
+
{buttonTitle}
|
|
42
|
+
</Component>
|
|
43
|
+
<Modal
|
|
44
|
+
variant="small"
|
|
45
|
+
{...rest}
|
|
46
|
+
title={modalTitle}
|
|
47
|
+
isOpen={open}
|
|
48
|
+
onClose={() => setOpen(false)}
|
|
49
|
+
actions={[
|
|
50
|
+
<Button
|
|
51
|
+
id="modal-confirm"
|
|
52
|
+
key="confirm"
|
|
53
|
+
variant="primary"
|
|
54
|
+
onClick={() => {
|
|
55
|
+
setOpen(false);
|
|
56
|
+
onContinue();
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{continueLabel}
|
|
60
|
+
</Button>,
|
|
61
|
+
<Button
|
|
62
|
+
id="modal-cancel"
|
|
63
|
+
key="cancel"
|
|
64
|
+
variant="secondary"
|
|
65
|
+
onClick={() => setOpen(false)}
|
|
66
|
+
>
|
|
67
|
+
{cancelLabel}
|
|
68
|
+
</Button>,
|
|
69
|
+
]}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</Modal>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormHelperText,
|
|
3
|
+
FormHelperTextProps,
|
|
4
|
+
HelperText,
|
|
5
|
+
HelperTextItem,
|
|
6
|
+
} from "@patternfly/react-core";
|
|
7
|
+
import { ExclamationCircleIcon } from "@patternfly/react-icons";
|
|
8
|
+
|
|
9
|
+
export type FormErrorTextProps = FormHelperTextProps & {
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const FormErrorText = ({ message, ...props }: FormErrorTextProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<FormHelperText {...props}>
|
|
16
|
+
<HelperText>
|
|
17
|
+
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
|
|
18
|
+
{message}
|
|
19
|
+
</HelperTextItem>
|
|
20
|
+
</HelperText>
|
|
21
|
+
</FormHelperText>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { FormGroup, FormGroupProps } from "@patternfly/react-core";
|
|
2
|
+
import { PropsWithChildren, ReactNode } from "react";
|
|
3
|
+
import { FieldError, FieldValues, Merge } from "react-hook-form";
|
|
4
|
+
import { FormErrorText } from "./FormErrorText";
|
|
5
|
+
import { HelpItem } from "./HelpItem";
|
|
6
|
+
|
|
7
|
+
export type FieldProps<T extends FieldValues = FieldValues> = {
|
|
8
|
+
label?: string;
|
|
9
|
+
name: string;
|
|
10
|
+
labelIcon?: string | ReactNode;
|
|
11
|
+
error?: FieldError | Merge<FieldError, T>;
|
|
12
|
+
isRequired: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type FormLabelProps = FieldProps & Omit<FormGroupProps, "label" | "labelIcon">;
|
|
16
|
+
|
|
17
|
+
export const FormLabel = ({
|
|
18
|
+
name,
|
|
19
|
+
label,
|
|
20
|
+
labelIcon,
|
|
21
|
+
error,
|
|
22
|
+
children,
|
|
23
|
+
...rest
|
|
24
|
+
}: PropsWithChildren<FormLabelProps>) => (
|
|
25
|
+
<FormGroup
|
|
26
|
+
label={label || name}
|
|
27
|
+
fieldId={name}
|
|
28
|
+
labelIcon={
|
|
29
|
+
labelIcon ? (
|
|
30
|
+
<HelpItem helpText={labelIcon} fieldLabelId={name} />
|
|
31
|
+
) : undefined
|
|
32
|
+
}
|
|
33
|
+
{...rest}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
{error && (
|
|
37
|
+
<FormErrorText data-testid={`${name}-helper`} message={error.message} />
|
|
38
|
+
)}
|
|
39
|
+
</FormGroup>
|
|
40
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Icon, Popover } from "@patternfly/react-core";
|
|
2
|
+
import { HelpIcon } from "@patternfly/react-icons";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { useHelp } from "../context/HelpContext";
|
|
5
|
+
|
|
6
|
+
type HelpItemProps = {
|
|
7
|
+
helpText: string | ReactNode;
|
|
8
|
+
fieldLabelId: string;
|
|
9
|
+
noVerticalAlign?: boolean;
|
|
10
|
+
unWrap?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const HelpItem = ({
|
|
14
|
+
helpText,
|
|
15
|
+
fieldLabelId,
|
|
16
|
+
noVerticalAlign = true,
|
|
17
|
+
unWrap = false,
|
|
18
|
+
}: HelpItemProps) => {
|
|
19
|
+
const { enabled } = useHelp();
|
|
20
|
+
return enabled ? (
|
|
21
|
+
<Popover bodyContent={helpText}>
|
|
22
|
+
<>
|
|
23
|
+
{!unWrap && (
|
|
24
|
+
<button
|
|
25
|
+
data-testid={`help-label-${fieldLabelId}`}
|
|
26
|
+
aria-label={fieldLabelId}
|
|
27
|
+
onClick={(e) => e.preventDefault()}
|
|
28
|
+
className="pf-v5-c-form__group-label-help"
|
|
29
|
+
>
|
|
30
|
+
<Icon isInline={noVerticalAlign}>
|
|
31
|
+
<HelpIcon />
|
|
32
|
+
</Icon>
|
|
33
|
+
</button>
|
|
34
|
+
)}
|
|
35
|
+
{unWrap && (
|
|
36
|
+
<Icon isInline={noVerticalAlign}>
|
|
37
|
+
<HelpIcon />
|
|
38
|
+
</Icon>
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
</Popover>
|
|
42
|
+
) : null;
|
|
43
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Bullseye, Spinner } from "@patternfly/react-core";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
export const KeycloakSpinner = () => {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Bullseye>
|
|
9
|
+
<Spinner aria-label={t("spinnerLoading")} />
|
|
10
|
+
</Bullseye>
|
|
11
|
+
);
|
|
12
|
+
};
|