@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.
Files changed (66) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +6 -0
  3. package/keycloak-theme/shared/keycloak-ui-shared/alerts/AlertPanel.tsx +43 -0
  4. package/keycloak-theme/shared/keycloak-ui-shared/alerts/Alerts.tsx +82 -0
  5. package/keycloak-theme/shared/keycloak-ui-shared/buttons/FormSubmitButton.tsx +47 -0
  6. package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +60 -0
  7. package/keycloak-theme/shared/keycloak-ui-shared/context/HelpContext.tsx +30 -0
  8. package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +97 -0
  9. package/keycloak-theme/shared/keycloak-ui-shared/context/environment.ts +50 -0
  10. package/keycloak-theme/shared/keycloak-ui-shared/continue-cancel/ContinueCancelModal.tsx +75 -0
  11. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormErrorText.tsx +23 -0
  12. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +40 -0
  13. package/keycloak-theme/shared/keycloak-ui-shared/controls/HelpItem.tsx +43 -0
  14. package/keycloak-theme/shared/keycloak-ui-shared/controls/KeycloakSpinner.tsx +12 -0
  15. package/keycloak-theme/shared/keycloak-ui-shared/controls/NumberControl.tsx +93 -0
  16. package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +122 -0
  17. package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordControl.tsx +71 -0
  18. package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordInput.tsx +50 -0
  19. package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +67 -0
  20. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +60 -0
  21. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +75 -0
  22. package/keycloak-theme/shared/keycloak-ui-shared/controls/keycloak-text-area/KeycloakTextArea.tsx +23 -0
  23. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +75 -0
  24. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +109 -0
  25. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +285 -0
  26. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +597 -0
  27. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +86 -0
  28. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/PaginatingTableToolbar.tsx +106 -0
  29. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +92 -0
  30. package/keycloak-theme/shared/keycloak-ui-shared/icons/IconMapper.tsx +63 -0
  31. package/keycloak-theme/shared/keycloak-ui-shared/index.ts +1 -0
  32. package/keycloak-theme/shared/keycloak-ui-shared/main.ts +96 -0
  33. package/keycloak-theme/shared/keycloak-ui-shared/masthead/DefaultAvatar.tsx +109 -0
  34. package/keycloak-theme/shared/keycloak-ui-shared/masthead/KeycloakDropdown.tsx +48 -0
  35. package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +161 -0
  36. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormPanel.tsx +29 -0
  37. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormTitle.tsx +28 -0
  38. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +98 -0
  39. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollPanel.tsx +21 -0
  40. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/form-title.module.css +4 -0
  41. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/scroll-form.module.css +8 -0
  42. package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +49 -0
  43. package/keycloak-theme/shared/keycloak-ui-shared/select/SingleSelect.tsx +89 -0
  44. package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +198 -0
  45. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +51 -0
  46. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/MultiInputComponent.tsx +146 -0
  47. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/OptionsComponent.tsx +63 -0
  48. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/SelectComponent.tsx +109 -0
  49. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextAreaComponent.tsx +23 -0
  50. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +32 -0
  51. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileFields.tsx +243 -0
  52. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileGroup.tsx +71 -0
  53. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +170 -0
  54. package/keycloak-theme/shared/keycloak-ui-shared/utils/ErrorBoundary.tsx +77 -0
  55. package/keycloak-theme/shared/keycloak-ui-shared/utils/createNamedContext.ts +11 -0
  56. package/keycloak-theme/shared/keycloak-ui-shared/utils/darkMode.ts +19 -0
  57. package/keycloak-theme/shared/keycloak-ui-shared/utils/errors.ts +55 -0
  58. package/keycloak-theme/shared/keycloak-ui-shared/utils/generateId.ts +1 -0
  59. package/keycloak-theme/shared/keycloak-ui-shared/utils/getRuleValue.ts +17 -0
  60. package/keycloak-theme/shared/keycloak-ui-shared/utils/isDefined.ts +3 -0
  61. package/keycloak-theme/shared/keycloak-ui-shared/utils/useFetch.ts +44 -0
  62. package/keycloak-theme/shared/keycloak-ui-shared/utils/useRequiredContext.ts +24 -0
  63. package/keycloak-theme/shared/keycloak-ui-shared/utils/useSetTimeout.ts +40 -0
  64. package/keycloak-theme/shared/keycloak-ui-shared/utils/useStorageItem.ts +51 -0
  65. package/keycloak-theme/shared/keycloak-ui-shared/utils/useStoredState.ts +38 -0
  66. package/package.json +31 -0
package/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ This is a re-package of https://github.com/keycloak/keycloak/tree/25.0.1/js/apps/account-ui
2
+ The license of the original project is Apache-2.0
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+
2
+ # @keycloakify/keycloak-ui-shared
3
+
4
+ > WARNING: This is a work in progress, do not attempt to use it yet.
5
+
6
+ This module is a repackage of `@keycloak/keycloak-ui-shared` meant to be used in the context of Keycloakify.
@@ -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
+ };